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
|
||||
|
||||
- **Backend**: Go + Gin + SQLite (go-sqlite3) + go-git
|
||||
- **Backend**: Go + Gin + MySQL + go-git
|
||||
- **Frontend**: Go html/template + HTMX + D3.js + diff2html + Tailwind CSS
|
||||
- **LLM**: OpenAI-compatible API (SSE streaming)
|
||||
- **Deploy**: Docker
|
||||
@@ -16,14 +16,17 @@ PR-Helper is a self-hosted web service that auto-generates PR descriptions from
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build (CGO required for SQLite)
|
||||
CGO_ENABLED=1 go build -o pr-helper .
|
||||
# Build
|
||||
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
|
||||
# Server listens on :8080
|
||||
|
||||
# Docker
|
||||
# Docker (includes MySQL container)
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
@@ -33,13 +36,13 @@ docker compose up --build
|
||||
handlers/ → HTTP handlers (pages + JSON API + SSE endpoints)
|
||||
services/ → Business logic (git ops, LLM calls, PDF gen, cache management)
|
||||
models/ → Data models (repository, settings, analysis)
|
||||
database/ → SQLite init and migrations
|
||||
database/ → MySQL init and migrations
|
||||
config/ → Configuration loading
|
||||
templates/ → Go HTML templates (layouts/, pages/, partials/)
|
||||
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**:
|
||||
- `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
|
||||
|
||||
- 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)
|
||||
- Database connection configured via `.env` file (see `.env.example`)
|
||||
|
||||
## Development Notes
|
||||
|
||||
|
||||
+2
-3
@@ -1,15 +1,14 @@
|
||||
# Build stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=1 go build -o pr-helper .
|
||||
RUN CGO_ENABLED=0 go build -o pr-helper .
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/pr-helper .
|
||||
COPY --from=builder /app/templates ./templates
|
||||
|
||||
+20
-2
@@ -3,8 +3,11 @@ package config
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -12,14 +15,27 @@ type Config struct {
|
||||
GinMode string
|
||||
DataDir string
|
||||
SessionSecret string
|
||||
MySQLHost string
|
||||
MySQLPort string
|
||||
MySQLUser string
|
||||
MySQLPassword string
|
||||
MySQLDatabase string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
// Load .env file if present (ignore error if missing)
|
||||
godotenv.Load()
|
||||
|
||||
cfg := &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
GinMode: getEnv("GIN_MODE", "debug"),
|
||||
DataDir: getEnv("DATA_DIR", "data"),
|
||||
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
|
||||
if cfg.SessionSecret == "" {
|
||||
@@ -33,8 +49,10 @@ func Load() *Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *Config) DBPath() string {
|
||||
return filepath.Join(c.DataDir, "pr-helper.db")
|
||||
// MySQLDSN returns the MySQL Data Source Name for go-sql-driver/mysql.
|
||||
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 {
|
||||
|
||||
+40
-78
@@ -4,7 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/HoHD/PR-Helper/models"
|
||||
)
|
||||
@@ -13,11 +13,14 @@ type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
func New(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||
func New(dsn string) (*DB, error) {
|
||||
conn, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db := &DB{conn: conn}
|
||||
if err := db.migrate(); err != nil {
|
||||
return nil, err
|
||||
@@ -39,111 +42,70 @@ func (db *DB) Conn() *sql.DB {
|
||||
func (db *DB) migrate() error {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
key VARCHAR(255) PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)`,
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS user_settings (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
key VARCHAR(255) 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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
url TEXT NOT NULL,
|
||||
local_path TEXT NOT NULL,
|
||||
size_bytes INTEGER DEFAULT 0,
|
||||
size_bytes BIGINT DEFAULT 0,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
repo_id INTEGER REFERENCES repositories(id),
|
||||
type TEXT NOT NULL,
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
repo_id BIGINT,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
base_ref TEXT NOT NULL,
|
||||
head_ref TEXT NOT NULL,
|
||||
result TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
result LONGTEXT,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
analysis_id INTEGER REFERENCES analyses(id),
|
||||
scope TEXT NOT NULL,
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
analysis_id BIGINT,
|
||||
scope VARCHAR(50) NOT NULL,
|
||||
scope_key TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
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 {
|
||||
if _, err := db.conn.Exec(s); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (db *DB) seedDefaults() error {
|
||||
for key, val := range models.DefaultSettings {
|
||||
_, 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 {
|
||||
log.Printf("warning: failed to seed setting %s: %v", key, err)
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
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:
|
||||
build: .
|
||||
ports:
|
||||
@@ -7,7 +23,16 @@ services:
|
||||
- pr-helper-data:/app/data
|
||||
environment:
|
||||
- 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
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
pr-helper-data:
|
||||
|
||||
@@ -6,13 +6,15 @@ require (
|
||||
github.com/gin-contrib/sessions v1.1.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
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
|
||||
golang.org/x/crypto v0.50.0
|
||||
)
|
||||
|
||||
require (
|
||||
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/ProtonMail/go-crypto v1.1.6 // 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/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.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
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/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-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/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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/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/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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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/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-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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
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
|
||||
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
|
||||
|
||||
@@ -73,7 +73,7 @@ func (h *GenerateHandler) Generate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 == "" {
|
||||
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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -244,7 +244,7 @@ func (h *ReposHandler) GetGraph(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
@@ -297,7 +297,7 @@ func (h *ReposHandler) GetDiff(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ func (h *ReviewHandler) Review(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
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 {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
Reference in New Issue
Block a user