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) }