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:
2026-06-20 22:40:46 +08:00
parent 6c60b767df
commit 00c1839635
14 changed files with 130 additions and 102 deletions
+14
View File
@@ -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
+12 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+25
View File
@@ -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:
+3 -1
View File
@@ -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
+6 -2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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()})
+1 -1
View File
@@ -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)
} }