Fix review findings, add per-project config and multi-target pushremote
Correctness and security fixes found by a review of the initial commit: - init decided whether a server repository existed from the *local* remote.origin.url, so an unlinked project directory skipped the confirmation and rm -rf'd the remote history. It now asks the server, and aborts when the server cannot be reached. - Project names and paths were interpolated unquoted into the remote shell command strings: a space split one path into two arguments and a backtick executed on the git server. Everything now goes through shq(), and chained remote commands use && so a failed cd cannot let the next command run in the login directory. - Bare `cd` panicked with an index-out-of-range and took down the shell; it now deselects the project. - Command-line mode set PRJ to the whole path below BASE, so `mgsh push` from a subdirectory staged only that subtree and addressed a bogus server path. It now truncates at the first path element. - `list` hardcoded owner and group "git git" in its regex and silently printed nothing on any server where the repositories are owned by someone else. - The config parser kept inline "#" comments in values although the README and the example file document them, so `mirror = true # ...` silently disabled mirroring. - ~/.mgshrc holds an API token but was created world-readable. - The mirror token was passed on git's command line, visible in the process table; it now goes through GIT_CONFIG_*. - tag, count and dist ran without a repository and operated on BASE. - checkout dropped its git options, because the dispatcher strips -x flags from the word list. - REPO was read with a plain `git config`, inheriting a foreign origin from an enclosing repository; it is now local-only and, being dead state otherwise, no longer recomputed on every prompt. - getkey consumed a single byte, leaving the rest of a typed answer in the tty queue where readline ran it as a command. - The REPL spun on any readline error that was neither EOF nor interrupt. - Tab completion cached an empty repository list after one failed ssh. - Startup did a blocking DNS lookup and three `git config --global` writes on every invocation. New: - A project may carry its own .mgshrc, overriding the global settings while it is active. Resolution order is ~/.mgshrc -> <project>/.mgshrc -> MGSH_*; base and the git identity keys stay global. It is read when the project changes, and `rescan` reloads it. - pushremote mirrors to any number of servers, configured as remote.<name>.url/key/type/visibility blocks. `pushremote` pushes to all of them, `pushremote @name ...` to a selection, and `remotes = ...` restricts and orders the set. Each target owns a git remote of the same name; a failing target no longer stops the others. - `config` shows the resolved configuration, its sources and the mirror targets with masked tokens; `config -k` lists the setting names. - gitkey was parsed and documented but never used. It is now the ssh identity for the git server, for mgsh's own ssh calls and, via GIT_SSH_COMMAND, for the git commands mgsh runs. - config, count, login and cloneall work from the command line too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+28
-8
@@ -25,7 +25,7 @@ func completer() *readline.PrefixCompleter {
|
||||
readline.PcItem("show", readline.PcItemDynamic(dynServerRepos)),
|
||||
readline.PcItem("list", readline.PcItem("-a")),
|
||||
readline.PcItem("push"),
|
||||
readline.PcItem("pushremote"),
|
||||
readline.PcItem("pushremote", readline.PcItemDynamic(dynRemoteNames)),
|
||||
readline.PcItem("pull"),
|
||||
readline.PcItem("fetch"),
|
||||
readline.PcItem("status", readline.PcItem("-a")),
|
||||
@@ -46,6 +46,7 @@ func completer() *readline.PrefixCompleter {
|
||||
),
|
||||
readline.PcItem("alias", readline.PcItemDynamic(dynAliasNames)),
|
||||
readline.PcItem("unalias", readline.PcItemDynamic(dynAliasNames)),
|
||||
readline.PcItem("config", readline.PcItem("-k")),
|
||||
readline.PcItem("rescan"),
|
||||
readline.PcItem("help"),
|
||||
readline.PcItem("quit"),
|
||||
@@ -80,22 +81,30 @@ func fetchServerRepos() {
|
||||
if serverFetched {
|
||||
return
|
||||
}
|
||||
serverFetched = true
|
||||
if lines, err := sshOut("/bin/ls ."); err == nil {
|
||||
for _, ln := range lines {
|
||||
if m := gitDirRe.FindStringSubmatch(strings.TrimSpace(ln)); m != nil {
|
||||
serverRepos = append(serverRepos, m[1])
|
||||
}
|
||||
lines, err := sshOut("/bin/ls .")
|
||||
if err != nil {
|
||||
// a transient failure (server down, no network) must not cache an
|
||||
// empty list for the rest of the session — the next Tab tries again
|
||||
return
|
||||
}
|
||||
var repos []string
|
||||
for _, ln := range lines {
|
||||
if m := gitDirRe.FindStringSubmatch(strings.TrimSpace(ln)); m != nil {
|
||||
repos = append(repos, m[1])
|
||||
}
|
||||
}
|
||||
// a missing ./archive is a permanent, unremarkable state: still cache
|
||||
var archives []string
|
||||
if lines, err := sshOut("/bin/ls archive"); err == nil {
|
||||
for _, ln := range lines {
|
||||
t := strings.TrimSpace(ln)
|
||||
if strings.HasSuffix(t, ".git.tar.gz") {
|
||||
serverArchives = append(serverArchives, strings.TrimSuffix(t, ".git.tar.gz"))
|
||||
archives = append(archives, strings.TrimSuffix(t, ".git.tar.gz"))
|
||||
}
|
||||
}
|
||||
}
|
||||
serverRepos, serverArchives = repos, archives
|
||||
serverFetched = true
|
||||
}
|
||||
|
||||
// rescanServer clears the cached server listing so the next completion (or use)
|
||||
@@ -109,6 +118,17 @@ func rescanServer() {
|
||||
func dynServerRepos(string) []string { fetchServerRepos(); return serverRepos }
|
||||
func dynServerArchives(string) []string { fetchServerRepos(); return serverArchives }
|
||||
|
||||
// dynRemoteNames offers the configured mirror targets as `@name` selectors for
|
||||
// `pushremote`, resolved against the active project's configuration.
|
||||
func dynRemoteNames(string) []string {
|
||||
targets, _ := cfg.mirrorTargets()
|
||||
var out []string
|
||||
for _, t := range targets {
|
||||
out = append(out, "@"+t.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dynBranches / dynTags list the active project's local branches / tags.
|
||||
func dynBranches(string) []string {
|
||||
if !isDir(DIR + "/.git") {
|
||||
|
||||
Reference in New Issue
Block a user