`push` runs `git add --all .`, so anything lying in the project gets committed, and with `mirror = true` it reaches a public server in the same breath. It is the one action in mgsh that cannot be undone: a deleted server repository comes back from an archive, a published credential does not. The staged diff is now scanned before the commit is made -- private keys, GitHub/GitLab/Slack/AWS/PyPI tokens, and credential-shaped assignments -- and a hit is shown with file and line before asking whether to continue. Declining leaves the changes staged but uncommitted, so removing the file and adding a .gitignore entry is all it takes. The hard part is not detection but silence. A scanner that cries wolf gets answered with a reflexive "y" and stops being a safety net, so values that are plainly environment references, dotted identifiers, constant names, template slots or masked stand-ins are filtered out. A test scans mgsh's own README and mgshrc.example -- both full of credential-shaped text -- and fails if either would trip the check. It caught the documentation for this very feature, which is why the README describes the sample output instead of reproducing it. For a line that legitimately looks like a credential there is `mgsh:allow`, which suppresses that one line; `secretscan = off` turns the check off entirely. Only an explicit "off" does that -- a typo in the setting leaves the safety net in place, which is what the new falsy() is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
4.4 KiB
Go
156 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",
|
|
"remoteurl", "remotekey", "remotetype", "remotevisibility",
|
|
"remotes", "mirror", "secretscan",
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|