Files
mgsh/config.go
T

178 lines
5.7 KiB
Go

package main
import (
"bufio"
"os"
"path/filepath"
"strings"
)
// Config holds all externally configurable settings. It is read entirely from
// ~/.mgshrc and then overlaid with MGSH_* environment variables — there are no
// built-in defaults. Missing required settings are a fatal error (see
// missingRequired).
type Config struct {
Base string // project base directory
GitHost string // git server host
GitPort string // ssh port
GitUser string // ssh user
GitPath string // remote path holding the bare repos
GitKey string // ssh key name (informational)
GitName string // git user.name to set globally ("" = leave alone)
GitEmail string // git user.email to set globally ("" = leave alone)
PushDefault string // git push.default to set globally ("" = leave alone)
Editor string // editor/opener used as fallback by `open` ("" = coda)
RemoteURL string // public mirror server base URL (Gitea/GitHub/GitLab)
RemoteKey string // API token for the mirror server (used by `pushremote`)
RemoteType string // "gitea"|"github"|"gitlab" (auto-detected when empty)
RemoteVis string // visibility of created repos: "private" (default)|"public"
Mirror string // truthy -> `push` also mirrors via `pushremote`
}
// requiredKeys lists the settings mgsh cannot run without.
var requiredKeys = []string{"base", "githost", "gitport", "gituser", "gitpath"}
// missingRequired reports which required settings are still unset. mgsh refuses
// to start while this is non-empty.
func (c Config) missingRequired() []string {
have := map[string]string{
"base": c.Base, "githost": c.GitHost, "gitport": c.GitPort,
"gituser": c.GitUser, "gitpath": c.GitPath,
}
var missing []string
for _, k := range requiredKeys {
if have[k] == "" {
missing = append(missing, k)
}
}
return missing
}
// configFile returns the path of the user config file (~/.mgshrc).
func configFile() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".mgshrc")
}
// loadConfig writes an empty ~/.mgshrc template on first run, then returns the
// configuration parsed from it, overlaid with MGSH_* environment variables.
// It applies no defaults; validation of required settings is the caller's job.
func loadConfig() Config {
path := configFile()
if !fileExists(path) {
writeConfigTemplate(path)
}
var c Config
if data, err := os.ReadFile(path); err == nil {
applyConfig(&c, parseConfig(string(data)))
}
applyEnv(&c)
return c
}
// writeConfigTemplate creates a blank, annotated ~/.mgshrc for the user to fill
// in, migrating any aliases from the legacy ~/.mgsh_aliases file. It writes no
// real values — mgsh has no built-in configuration.
func writeConfigTemplate(path string) {
var b strings.Builder
b.WriteString("# mgsh configuration — fill in the required settings, then run mgsh again.\n")
b.WriteString("# Format: 'key = value' (or 'key: value'); '#' starts a comment.\n")
b.WriteString("# MGSH_* environment variables override these settings.\n\n")
b.WriteString("# --- required ---\n")
b.WriteString("base =\n")
b.WriteString("githost =\n")
b.WriteString("gitport =\n")
b.WriteString("gituser =\n")
b.WriteString("gitpath =\n\n")
b.WriteString("# --- optional ---\n")
b.WriteString("# gitkey =\n")
b.WriteString("# gitname = Your Name\n")
b.WriteString("# gitemail = you@example.com\n")
b.WriteString("# pushdefault = matching\n")
b.WriteString("# editor = code\n")
legacy := readLegacyAliases()
if blk := aliasBlock(legacy); blk != "" {
b.WriteString("\n" + blk)
}
if err := os.WriteFile(path, []byte(b.String()), 0644); err != nil {
errorln("could not create " + path + ": " + err.Error())
return
}
if len(legacy) > 0 {
os.Remove(legacyAliasFile()) // aliases now live in ~/.mgshrc
}
}
// parseConfig reads simple `key = value` (or `key: value`) lines, ignoring
// blank lines, `#` comments and `alias` definitions. Keys are lower-cased;
// values are unquoted.
func parseConfig(s string) map[string]string {
m := map[string]string{}
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if f := strings.Fields(line); len(f) > 0 && f[0] == "alias" {
continue // alias definitions are handled by the alias loader
}
i := strings.IndexAny(line, "=:")
if i < 0 {
continue
}
key := strings.ToLower(strings.TrimSpace(line[:i]))
val := strings.Trim(strings.TrimSpace(line[i+1:]), "\"'")
m[key] = val
}
return m
}
func applyConfig(c *Config, m map[string]string) {
set := func(key string, dst *string) {
if v, ok := m[key]; ok && v != "" {
*dst = v
}
}
set("base", &c.Base)
set("githost", &c.GitHost)
set("gitport", &c.GitPort)
set("gituser", &c.GitUser)
set("gitpath", &c.GitPath)
set("gitkey", &c.GitKey)
set("gitname", &c.GitName)
set("gitemail", &c.GitEmail)
set("pushdefault", &c.PushDefault)
set("editor", &c.Editor)
set("remoteurl", &c.RemoteURL)
set("remotekey", &c.RemoteKey)
set("remotetype", &c.RemoteType)
set("remotevisibility", &c.RemoteVis)
set("mirror", &c.Mirror)
}
func applyEnv(c *Config) {
env := func(key string, dst *string) {
if v := os.Getenv(key); v != "" {
*dst = v
}
}
env("MGSH_BASE", &c.Base)
env("MGSH_GITHOST", &c.GitHost)
env("MGSH_GITPORT", &c.GitPort)
env("MGSH_GITUSER", &c.GitUser)
env("MGSH_GITPATH", &c.GitPath)
env("MGSH_GITKEY", &c.GitKey)
env("MGSH_GITNAME", &c.GitName)
env("MGSH_GITEMAIL", &c.GitEmail)
env("MGSH_PUSHDEFAULT", &c.PushDefault)
env("MGSH_EDITOR", &c.Editor)
env("MGSH_REMOTEURL", &c.RemoteURL)
env("MGSH_REMOTEKEY", &c.RemoteKey)
env("MGSH_REMOTETYPE", &c.RemoteType)
env("MGSH_REMOTEVISIBILITY", &c.RemoteVis)
env("MGSH_MIRROR", &c.Mirror)
}