Refactor codebase for Phase 3
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user