182 lines
6.6 KiB
Go
182 lines
6.6 KiB
Go
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 {
|
|
if len(args) > 0 && args[0] == "users" {
|
|
return cliServerUsers(args[1:])
|
|
}
|
|
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
|
|
}
|