mikeandClaude Opus 5 689a3fe594 Make the ssh layer injectable and cover the destructive server commands
The remote commands are the only code in mgsh that can destroy data, and
they were the least verifiable: each one is a single string handed to a
login shell, so a missing quote silently changes which paths it touches.
Every one of them now goes through the sshExec variable, and yesno is a
variable too, so a test can record what would have been sent and answer
the confirmations without a terminal.

The tests pin down what the previous commit fixed by reasoning alone:
that a declined or unreachable `init` sends no rm -rf, that a project
named "my 'weird' project" reaches the server fully quoted, and that
archive's cp/tar/rm sequence is named, ordered and quoted correctly.
push is driven end to end against a real local bare repository.

archive also gained the server-side existence check that init, clone and
show already had, so a project that was never pushed reports that instead
of failing inside cp -r.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 11:38:22 +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%