Files
mgsh/show_config.go
T
mikeandClaude Opus 5 2a622046f2 Define mirror targets one way: remote.<name>.<field>
There were two spellings for the same thing -- a flat
remoteurl/remotekey/remotetype/remotevisibility set for a single server,
and remote.<name>.* blocks for several. The flat one is gone; every
target, including a lone one, is now a named block with the fields url,
key, type and visibility.

An existing ~/.mgshrc is converted on the next start. Only the key is
rewritten, so values, comments, alignment, commented-out lines and the
file's 0600 mode survive untouched, and mgsh prints each rename rather
than doing it quietly. The target is named "public", which is what the
old settings called the git remote they created, so a converted setup
keeps pushing to the same place under the same remote name. A file that
carries both spellings keeps what the new one says.

The environment follows the same shape: MGSH_REMOTEURL and friends are
replaced by MGSH_REMOTE_<NAME>_<FIELD>, so MGSH_REMOTE_GITLAB_KEY sets
remote.gitlab.key. The field is read from the end of the variable name,
which leaves target names free to contain underscores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:00:26 +02:00

155 lines
4.4 KiB
Go

package main
// show_config.go — the `config` command: print the configuration mgsh actually
// resolved, and where it came from. With three layers (~/.mgshrc, the project's
// .mgshrc, MGSH_*) and mirror targets that can be added or narrowed per
// project, "what is in effect right now" is otherwise guesswork.
import (
"fmt"
"os"
"sort"
"strings"
)
// configItem is one reported setting.
type configItem struct{ key, value string }
// showConfig prints the effective configuration, marking every value that does
// not come from the global file with its source.
func showConfig() {
global := configFile()
project := ""
if PRJ != "" && fileExists(DIR+"/"+projectRC) {
project = DIR + "/" + projectRC
}
fmt.Printf("%s %s\n", col(cGray, "global "), col(cCyan, global))
if project != "" {
fmt.Printf("%s %s\n", col(cGray, "project"), col(cCyan, project))
} else if PRJ != "" {
fmt.Printf("%s %s\n", col(cGray, "project"), col(cGray, "no "+projectRC+" in "+PRJ))
}
fmt.Println()
items := []configItem{
{"base", cfg.Base},
{"githost", cfg.GitHost},
{"gitport", cfg.GitPort},
{"gituser", cfg.GitUser},
{"gitpath", cfg.GitPath},
{"gitkey", cfg.GitKey},
{"gitname", cfg.GitName},
{"gitemail", cfg.GitEmail},
{"pushdefault", cfg.PushDefault},
{"editor", cfg.Editor},
{"mirror", cfg.Mirror},
{"secretscan", cfg.SecretScan},
{"remotes", cfg.RemoteNames},
}
// which keys the active project's file actually sets, for the source column
fromProject := map[string]bool{}
if project != "" {
if data, err := os.ReadFile(project); err == nil {
for k := range parseConfig(string(data)) {
fromProject[k] = true
}
}
}
for _, it := range items {
if it.value == "" {
continue
}
src := ""
switch {
case os.Getenv(envName(it.key)) != "":
src = col(cYellow, " (env)")
case fromProject[it.key]:
src = col(cYellow, " ("+projectRC+")")
}
fmt.Printf(" %s%s%s\n", col(cGreen, padRight(it.key, 14)), it.value, src)
}
if k := sshKeyPath(); k != "" {
fmt.Printf(" %s%s\n", col(cGray, padRight("ssh identity", 14)), col(cGray, k))
}
fmt.Printf(" %s%s\n", col(cGray, padRight("clone url", 14)), col(cGray, URL))
// the project's real origin: it can differ from what the current settings
// would produce, e.g. after moving the server or editing a project .mgshrc
if o := originURL(); o != "" {
fmt.Printf(" %s%s\n", col(cGray, padRight("origin", 14)), col(cGray, o))
}
showRemotes()
}
// showRemotes lists the `pushremote` targets in push order, with the tokens
// masked — `config` is the kind of output that ends up pasted into a bug report.
func showRemotes() {
targets, incomplete := cfg.mirrorTargets()
fmt.Println()
if len(targets) == 0 && len(incomplete) == 0 {
fmt.Println(col(cGray, "no pushremote targets configured"))
return
}
fmt.Println(col(cGray, "pushremote targets (in push order):"))
for _, t := range targets {
vis := "private"
if strings.EqualFold(strings.TrimSpace(t.Vis), "public") {
vis = "public"
}
kind := t.Type
if kind == "" {
kind = remoteKindName(detectRemoteKind(t.URL, "")) + " (detected)"
}
fmt.Printf(" %s%s %s\n",
col(cGreen, padRight("@"+t.Name, 14)), t.URL,
col(cGray, kind+", "+vis+", key "+maskSecret(t.Key)))
}
for _, n := range incomplete {
fmt.Printf(" %s%s\n", col(cRed, padRight("@"+n, 14)), col(cRed, "incomplete: url or key missing"))
}
}
// remoteKindName renders a remoteKind for display.
func remoteKindName(k remoteKind) string {
switch k {
case kindGitHub:
return "github"
case kindGitLab:
return "gitlab"
default:
return "gitea"
}
}
// maskSecret reduces a token to a recognisable but useless stub.
func maskSecret(s string) string {
if s == "" {
return "(unset)"
}
if len(s) <= 4 {
return strings.Repeat("*", len(s))
}
return s[:2] + strings.Repeat("*", len(s)-4) + s[len(s)-2:]
}
// envName maps a config key to its MGSH_* environment variable.
func envName(key string) string { return "MGSH_" + strings.ToUpper(key) }
// configKeys lists every flat setting name — that is, every key that also has
// an MGSH_* override. The `remote.<name>.*` targets are a pattern, not a fixed
// set, and are reported separately.
func configKeys() []string {
keys := []string{
"base", "githost", "gitport", "gituser", "gitpath", "gitkey",
"gitname", "gitemail", "pushdefault", "editor",
"remotes", "mirror", "secretscan",
}
sort.Strings(keys)
return keys
}