mikeandClaude Opus 5 ae8c7a3ec0 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>
2026-07-26 11:37:49 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00
2026-07-26 06:38:01 +02:00

mgsh — git shell

A small interactive shell / command-line wrapper around a self-hosted bare git server reachable over ssh. It manages a flat set of projects living under a base directory (default $HOME/src, or /db/src on Linux). Go port of the original Perl mgsh (mgsh.perl).

Build

./build.sh          # builds ./mgsh and bumps the patch version by 0.0.1
go build -o mgsh .  # plain build, keeps the default version

build.sh reads version.txt, increments the patch component, injects it via -ldflags -X main.VERSION, and writes it back — so version.txt always holds the version of the binary just built. Dependencies are fetched via Go modules (go.mod / go.sum) on first build.

Run the tests with go test ./....

Usage

Launch mgsh for the interactive shell, or run a single command directly from a project directory, e.g. mgsh push "message", mgsh status, mgsh log.

The interactive prompt is colored (Catppuccin Mocha) and shows the active project, its git branch and a * dirty marker:

< src/myproject (master*) >

Features: command history (~/.mgsh_history), Tab completion (commands, local projects for cd/open, server repos for clone/show, branches/tags for checkout/tag, filesystem paths for dist), and colored list/log/error output.

Exit with quit, exit, Ctrl-D, or Ctrl-C on an empty line.

Shell escape

Unknown commands are not forwarded to a shell. To run a shell command, prefix it with !:

< src/myproject > !ls -la

Commands

Run help for the full list. Highlights:

command description
cd [project] change project
push [comment] commit everything and push to the server
pushremote [desc] mirror the repo to a public server (gitea/github/gitlab)
pull / fetch pull / fetch from the server
status [-a] / diff short git status (-a: overview of all projects)
overview dirty / ahead-behind summary of all projects
log show the project log
edit [n] interactive rebase of the last n commits
clone [-a] <repo> clone a repository (or archive) from the server
list [-a] [pattern] list repositories on the server
show <repo> show a repository log directly on the server
archive [comment] snapshot the server-side repo into ./archive
init make a new repository from the current directory
tag [add/checkout/delete] manage tags
alias [name [cmd]] list, show or define a command alias
unalias <name> remove a command alias
rescan refresh the cached server repository list
!<command> run <command> in the shell

Aliases

alias <name> '<command>' defines a reusable shortcut, persisted to ~/.mgshrc and reloaded on every start. The expansion is itself a mgsh command line and may reference the alias arguments:

placeholder meaning
$1$N the Nth argument (empty if unset)
$* / $@ all arguments, space-joined

When the expansion contains no placeholder, the arguments are appended (classic shell-alias behaviour). Because unknown commands are not forwarded to a shell, a shell command inside an alias needs the ! prefix:

alias co 'checkout $1'    # co v2         -> checkout v2   (builtin)
alias p  'push $*'        # p fixed bug   -> push fixed bug (builtin)
alias ec '!echo $1'       # ec hello      -> echo hello    (shell)

alias with no arguments lists all aliases, alias <name> shows one, and unalias <name> removes it. Aliases cannot shadow builtin commands.

Public mirror (pushremote)

Besides the internal ssh git server, pushremote mirrors the active project to a public hosting server (Gitea, GitHub or GitLab) over its REST API. It reads two settings from ~/.mgshrc:

remoteurl        = https://git.example.com   # base URL of the server
remotekey        = <personal-access-token>   # API token
# remotetype     = gitea                     # optional; auto-detected from remoteurl
# remotevisibility = private                 # visibility of created repos (default private)
# mirror         = true                      # `push` also mirrors via pushremote

pushremote authenticates with the token, creates the repository (named after the current project) if it does not exist yet, adds a credential-free remote named public, and pushes all branches and tags. New repositories are private unless remotevisibility = public; any words after the command (pushremote <description>) are set as the repository description on creation. The token is sent as a one-shot HTTP auth header, never written into the repo's git config. The provider is auto-detected from remoteurl (github.com → GitHub, gitlab* → GitLab, otherwise Gitea) and can be forced with remotetype. Set mirror = true to have every push mirror automatically.

Configuration

mgsh has no built-in defaults. Configuration comes entirely from ~/.mgshrc (overlaid with MGSH_* environment variables). On first run mgsh writes a blank, annotated ~/.mgshrc template (migrating any aliases from a pre-4.x ~/.mgsh_aliases) and then exits with an error until the required settings — base, githost, gitport, gituser, gitpath — are filled in.

It uses simple key = value (or key: value) lines (# comments allowed); alias definitions live in the same file:

# --- required ---
base        = /Users/me/src
githost     = git.example.com
gitport     = 22
gituser     = git
gitpath     = /home/git

# --- optional ---
gitname     = Your Name
gitemail    = you@example.com
pushdefault = matching
editor      = code            # fallback opener for `open`

alias co 'checkout $1'

Environment overrides: MGSH_BASE, MGSH_GITHOST, MGSH_GITPORT, MGSH_GITUSER, MGSH_GITPATH, MGSH_GITKEY, MGSH_GITNAME, MGSH_GITEMAIL, MGSH_PUSHDEFAULT, MGSH_EDITOR, MGSH_REMOTEURL, MGSH_REMOTEKEY, MGSH_REMOTETYPE, MGSH_REMOTEVISIBILITY, MGSH_MIRROR.

See mgshrc.example for an annotated template.

S
Description
No description provided
Readme
320 KiB
4.0.70
Latest
2026-08-11 16:19:23 +02:00
Languages
Go 99.4%
Shell 0.6%