Refactor codebase for Phase 3

This commit is contained in:
2026-06-08 13:34:53 -05:00
parent 6320f51295
commit c32e08be38
32 changed files with 4718 additions and 3323 deletions
+341
View File
@@ -0,0 +1,341 @@
package app
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"gitocean/internal/backup"
appconfig "gitocean/internal/config"
"gitocean/internal/dbutil"
"gitocean/internal/gitutil"
"golang.org/x/crypto/bcrypt"
"io"
"net/http"
"net/url"
"os/exec"
"strings"
)
// ---------------- CLI helpers ----------------
func apiRequest(ctx context.Context, server, method, path, token string, body any, out any) error {
server = strings.TrimRight(server, "/")
var rdr io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, server+path, rdr)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
var e struct {
Error string `json:"error"`
}
if json.Unmarshal(b, &e) == nil && e.Error != "" {
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("%s\nYour session may have expired. Run:\n gitocean login", e.Error)
}
return fmt.Errorf("%s", e.Error)
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
if out != nil && len(b) > 0 {
if err := json.Unmarshal(b, out); err != nil {
return err
}
}
return nil
}
func envServer() string {
return appconfig.EnvServer()
}
func configPath() string {
return appconfig.ClientConfigPath()
}
func loadConfig() (Config, error) {
return appconfig.LoadClient()
}
func saveConfig(cfg Config) error {
return appconfig.SaveClient(cfg)
}
func approveGitCredential(cfg Config) {
u, err := url.Parse(cfg.Server)
if err != nil || u.Host == "" || u.Scheme == "" {
return
}
cmd := exec.Command("git", "credential", "approve")
cmd.Stdin = strings.NewReader(fmt.Sprintf("protocol=%s\nhost=%s\nusername=%s\npassword=%s\n\n", u.Scheme, u.Host, cfg.Username, cfg.Token))
_ = cmd.Run()
}
func prompt(label string) string {
fmt.Print(label)
var s string
fmt.Scanln(&s)
return strings.TrimSpace(s)
}
func printJSON(v any) {
b, _ := json.MarshalIndent(v, "", " ")
fmt.Println(string(b))
}
func splitOwnerRepo(s string) (string, string, error) {
parts := strings.Split(s, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("expected OWNER/REPO, got %q", s)
}
return strings.ToLower(parts[0]), strings.ToLower(parts[1]), nil
}
func splitRepoBranch(s string) (owner, repo, branch string, err error) {
left, branch, ok := strings.Cut(s, ":")
if !ok || branch == "" {
return "", "", "", fmt.Errorf("expected OWNER/REPO:BRANCH, got %q", s)
}
owner, repo, err = splitOwnerRepo(left)
return owner, repo, branch, err
}
func parseRepoCreateArgs(args []string) (name, visibility, description string, jsonOut bool, err error) {
pub, priv := false, false
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--public" || arg == "-public":
pub = true
case arg == "--private" || arg == "-private":
priv = true
case arg == "--json":
jsonOut = true
case arg == "--description":
if i+1 >= len(args) {
return "", "", "", false, errors.New("--description requires a value")
}
i++
description = args[i]
case strings.HasPrefix(arg, "--description="):
description = strings.TrimPrefix(arg, "--description=")
default:
if strings.HasPrefix(arg, "-") {
return "", "", "", false, fmt.Errorf("unknown flag %s", arg)
}
if name != "" {
return "", "", "", false, errors.New("usage: gitocean repo create NAME (--public|--private)")
}
name = arg
}
}
if name == "" || pub == priv {
return "", "", "", false, errors.New("usage: gitocean repo create NAME (--public|--private)")
}
visibility = "private"
if pub {
visibility = "public"
}
return name, visibility, description, jsonOut, nil
}
func parseRepoPublishArgs(args []string) (name, visibility, description, remoteName, branch string, err error) {
remoteName = "origin"
branch = "main"
filtered := []string{}
for i := 0; i < len(args); i++ {
switch args[i] {
case "--remote":
if i+1 >= len(args) {
return "", "", "", "", "", errors.New("--remote requires a value")
}
i++
remoteName = args[i]
case "--branch":
if i+1 >= len(args) {
return "", "", "", "", "", errors.New("--branch requires a value")
}
i++
branch = args[i]
default:
filtered = append(filtered, args[i])
}
}
name, visibility, description, _, err = parseRepoCreateArgs(filtered)
if err != nil {
return "", "", "", "", "", err
}
return name, visibility, description, remoteName, branch, nil
}
func removeBoolFlag(args *[]string, flagName string) bool {
in := *args
out := in[:0]
found := false
for _, arg := range in {
if arg == flagName {
found = true
continue
}
out = append(out, arg)
}
*args = out
return found
}
func repoCloneURL(server, owner, repo string) string {
return fmt.Sprintf("%s/%s/%s.git", strings.TrimRight(server, "/"), owner, repo)
}
func printRepoCreated(server string, repo Repository) {
remote := repoCloneURL(server, repo.Owner, repo.Name)
fmt.Printf("Created %s repository %s/%s\n\n", repo.Visibility, repo.Owner, repo.Name)
fmt.Println("Clone:")
fmt.Printf(" git clone %s\n\n", remote)
fmt.Println("Add existing repo:")
fmt.Printf(" git remote add origin %s\n", remote)
fmt.Printf(" git branch -M %s\n", repo.DefaultBranch)
fmt.Printf(" git push -u origin %s\n", repo.DefaultBranch)
}
func printRepoView(server string, repo Repository) {
fmt.Printf("%s/%s\n", repo.Owner, repo.Name)
fmt.Printf("Visibility: %s\n", repo.Visibility)
fmt.Printf("Default branch: %s\n", repo.DefaultBranch)
fmt.Printf("Archived: %v\n", repo.Archived)
if repo.Description != "" {
fmt.Printf("Description: %s\n", repo.Description)
}
fmt.Println("\nClone:")
fmt.Printf(" git clone %s\n", repoCloneURL(server, repo.Owner, repo.Name))
}
func printRepoTable(repos []Repository) {
fmt.Printf("%-28s %-10s %-8s %s\n", "REPOSITORY", "VISIBILITY", "ARCHIVED", "DESCRIPTION")
for _, r := range repos {
fmt.Printf("%-28s %-10s %-8v %s\n", r.Owner+"/"+r.Name, r.Visibility, r.Archived, r.Description)
}
}
func printRefs(kind string, refs []RefInfo) {
fmt.Printf("%-28s %-10s %-20s %s\n", strings.ToUpper(strings.TrimSuffix(kind, "es")), "COMMIT", "DATE", "MESSAGE")
for _, r := range refs {
commit := r.Commit
if len(commit) > 10 {
commit = commit[:10]
}
fmt.Printf("%-28s %-10s %-20s %s\n", r.Name, commit, r.Date.Format("2006-01-02 15:04"), r.Message)
}
}
func printPR(pr PullRequest) {
fmt.Printf("#%d %s: %s\n", pr.Number, pr.Status, pr.Title)
fmt.Printf("Author: %s\n", pr.Author)
fmt.Printf("From: %s/%s:%s\n", pr.SourceOwner, pr.SourceRepo, pr.SourceBranch)
fmt.Printf("To: %s/%s:%s\n", pr.TargetOwner, pr.TargetRepo, pr.TargetBranch)
if pr.Description != "" {
fmt.Printf("\n%s\n", pr.Description)
}
}
func apiTextRequest(ctx context.Context, server, path, token string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(server, "/")+path, nil)
if err != nil {
return "", err
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
return string(b), nil
}
func defaultServerConfigPath() string {
return appconfig.DefaultServerConfigPath()
}
func loadServerConfig(path string) (ServerConfig, error) {
return appconfig.LoadServer(path)
}
func saveServerConfig(path string, cfg ServerConfig) error {
return appconfig.SaveServer(path, cfg)
}
func promptDefault(label, def string) string {
fmt.Printf("%s [%s]: ", label, def)
var s string
fmt.Scanln(&s)
if strings.TrimSpace(s) == "" {
return def
}
return strings.TrimSpace(s)
}
func createUserDirect(db *sql.DB, email, username, password string, isAdmin bool) error {
if !strings.Contains(email, "@") || !usernameRE.MatchString(username) || len(password) < 8 {
return errors.New("invalid email, username, or password")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = db.Exec(`INSERT INTO users (email, username, password_hash, is_admin) VALUES (?, ?, ?, ?)`, email, username, string(hash), isAdmin)
return err
}
func gitRefs(repoPath, prefix string) ([]RefInfo, error) {
return gitutil.Refs(repoPath, prefix)
}
func createBackup(cfg ServerConfig, file string) error {
return backup.Create(cfg, file)
}
func restoreBackup(cfg ServerConfig, file string) error {
return backup.Restore(cfg, file)
}
func mysqlCLIArgs(dsn string) ([]string, string, error) {
return backup.MySQLCLIArgs(dsn)
}
func copyDir(src, dst string) error {
return backup.CopyDir(src, dst)
}
func isDuplicateColumnError(err error) bool {
return dbutil.IsDuplicateColumnError(err)
}