Refactor codebase for Phase 3
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitocean/internal/dbutil"
|
||||
)
|
||||
|
||||
// ---------------- Server boot and migrations ----------------
|
||||
|
||||
func runServer(args []string) error {
|
||||
fs := flag.NewFlagSet("server", flag.ExitOnError)
|
||||
configFile := fs.String("config", defaultServerConfigPath(), "server config file")
|
||||
addr := fs.String("addr", "", "HTTP listen address")
|
||||
dsn := fs.String("dsn", os.Getenv("GITOOCEAN_DSN"), "MySQL DSN")
|
||||
storage := fs.String("storage", "", "storage directory")
|
||||
publicURL := fs.String("public-url", "", "public server URL")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, _ := loadServerConfig(*configFile)
|
||||
if *addr != "" {
|
||||
cfg.Addr = *addr
|
||||
}
|
||||
if *dsn != "" {
|
||||
cfg.MySQLDSN = *dsn
|
||||
}
|
||||
if *storage != "" {
|
||||
cfg.Storage = *storage
|
||||
}
|
||||
if *publicURL != "" {
|
||||
cfg.PublicURL = *publicURL
|
||||
}
|
||||
if cfg.Addr == "" {
|
||||
cfg.Addr = ":8080"
|
||||
}
|
||||
if cfg.Storage == "" {
|
||||
cfg.Storage = defaultStorage
|
||||
}
|
||||
if cfg.PublicURL == "" {
|
||||
cfg.PublicURL = defaultServerURL
|
||||
}
|
||||
if cfg.MySQLDSN == "" {
|
||||
return errors.New("--dsn, GITOOCEAN_DSN, or server config mysql_dsn is required; run gitocean init")
|
||||
}
|
||||
|
||||
db, err := openMySQLAndCreateDatabaseIfMissing(cfg.MySQLDSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if err := migrate(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(cfg.Storage, "repos"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := &Server{db: db, storage: cfg.Storage, publicURL: strings.TrimRight(cfg.PublicURL, "/")}
|
||||
fmt.Printf("gitocean listening on %s, storage=%s, public_url=%s\n", cfg.Addr, cfg.Storage, s.publicURL)
|
||||
return http.ListenAndServe(cfg.Addr, s)
|
||||
}
|
||||
|
||||
func openMySQLAndCreateDatabaseIfMissing(dsn string) (*sql.DB, error) {
|
||||
return dbutil.OpenMySQLAndCreateDatabaseIfMissing(dsn)
|
||||
}
|
||||
|
||||
func quoteMySQLIdentifier(name string) string {
|
||||
return dbutil.QuoteMySQLIdentifier(name)
|
||||
}
|
||||
|
||||
func migrate(db *sql.DB) error {
|
||||
stmts := []string{
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
username VARCHAR(32) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
revoked_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_auth_tokens_user_id (user_id),
|
||||
INDEX idx_auth_tokens_expires_at (expires_at),
|
||||
CONSTRAINT fk_auth_tokens_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS repositories (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_user_id BIGINT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
visibility ENUM('public','private') NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
default_branch VARCHAR(200) NOT NULL DEFAULT 'main',
|
||||
archived BOOLEAN NOT NULL DEFAULT false,
|
||||
forked_from_repository_id BIGINT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_owner_name (owner_user_id, name),
|
||||
INDEX idx_repositories_name (name),
|
||||
CONSTRAINT fk_repositories_owner FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_repositories_fork FOREIGN KEY (forked_from_repository_id) REFERENCES repositories(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS repository_collaborators (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
repository_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
role ENUM('read','write') NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_repo_user (repository_id, user_id),
|
||||
CONSTRAINT fk_collab_repo FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_collab_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS pull_requests (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
target_repository_id BIGINT NOT NULL,
|
||||
number INT NOT NULL,
|
||||
author_user_id BIGINT NOT NULL,
|
||||
source_repository_id BIGINT NOT NULL,
|
||||
source_branch VARCHAR(200) NOT NULL,
|
||||
target_branch VARCHAR(200) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status ENUM('open','closed','merged') NOT NULL DEFAULT 'open',
|
||||
closed_at TIMESTAMP NULL,
|
||||
merged_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_target_number (target_repository_id, number),
|
||||
INDEX idx_pr_target (target_repository_id),
|
||||
INDEX idx_pr_source (source_repository_id),
|
||||
CONSTRAINT fk_pr_target FOREIGN KEY (target_repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pr_source FOREIGN KEY (source_repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pr_author FOREIGN KEY (author_user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
`CREATE TABLE IF NOT EXISTS pull_request_comments (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
pull_request_id BIGINT NOT NULL,
|
||||
author_user_id BIGINT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_pr_comments_pr (pull_request_id),
|
||||
CONSTRAINT fk_pr_comments_pr FOREIGN KEY (pull_request_id) REFERENCES pull_requests(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pr_comments_author FOREIGN KEY (author_user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
alterations := []string{
|
||||
`ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT false`,
|
||||
`ALTER TABLE repositories ADD COLUMN description TEXT NULL`,
|
||||
`ALTER TABLE repositories ADD COLUMN default_branch VARCHAR(200) NOT NULL DEFAULT 'main'`,
|
||||
`ALTER TABLE repositories ADD COLUMN archived BOOLEAN NOT NULL DEFAULT false`,
|
||||
}
|
||||
for _, stmt := range alterations {
|
||||
if _, err := db.Exec(stmt); err != nil && !isDuplicateColumnError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user