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>
`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>
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>