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>
155 lines
4.4 KiB
Go
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
|
|
}
|