The README had no complete list of settings, so gitkey being inert and `remotes` being undocumented were invisible. It now carries a reference table of every setting with its meaning and whether a project .mgshrc may override it, kept honest by a test that checks each listed key really has an MGSH_* override. It also documents what mgsh expects of the git server, which was assumed but never written down: the login directory of gituser *is* gitpath — every remote command runs there without a cd — and archive/ has to exist before `archive` and `clone -a` can work. Plus the per-project config, the multi-target pushremote forms, `config`, and how the project is derived from the working directory in command-line mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
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. Go port of the original Perl mgsh (mgsh.perl).
Contents
- Build · Usage · Commands · Aliases
- Public mirror (
pushremote) - Configuration · Settings reference · Per-project configuration
- Git server layout
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.
In command-line mode the project is taken from the working directory: anywhere
below <base>/myproject — however deep — the active project is myproject, and
the command applies to the whole project, not the subdirectory you happen to
stand in. Outside base no project is selected. mgsh <project> starts the
interactive shell with that project preselected.
The commands available directly from the shell are clone, init, log,
push, pushremote, list, tag, archive, show, open, pull, fetch,
status, diff, overview, config, count, login and cloneall; every
other command is interactive-only.
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, mirror targets for pushremote, filesystem paths for dist),
and colored list/log/error output.
The server repository list is fetched once per session on the first Tab that
needs it; rescan refreshes it (and reloads the configuration).
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 (no argument: back to the base) |
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 |
config [-k] |
show the effective configuration and its sources |
rescan |
reload the config, refresh the cached repo list |
!<command> |
run <command> in the shell |
Commands that touch a repository (push, pull, log, diff, tag, dist,
checkout, edit, …) require an active project with a .git; they refuse to
run rather than silently operating on the base directory. checkout and diff
forward their arguments to git unchanged, so checkout -b topic works.
config is the quickest way to see what mgsh actually resolved — which server,
which mirror targets, and which of them came from the project's own .mgshrc:
< src/myproject > config
global /Users/me/.mgshrc
project /Users/me/src/myproject/.mgshrc
base /Users/me/src
githost git.example.com
gitport 22
gituser git
gitpath /home/git
editor code (.mgshrc)
remotes hub (.mgshrc)
clone url ssh://git@git.example.com:22/home/git
pushremote targets (in push order):
@hub https://github.com github (detected), public, key gh***************xk
Tokens are masked, so the output is safe to paste into a bug report.
config -k prints just the setting names, one per line.
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
one or more public hosting servers (Gitea, GitHub or GitLab) over their REST
API. A single server is configured flat:
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
Several servers get one remote.<name>.* block each:
remote.gitea.url = https://git.example.com
remote.gitea.key = <personal-access-token>
remote.hub.url = https://github.com
remote.hub.key = <personal-access-token>
remote.hub.visibility = public
# remotes = gitea, hub # optional: restrict and order the set
| command | pushes to |
|---|---|
pushremote |
every configured target, in order |
pushremote @hub |
only hub |
pushremote @hub @gitea |
those two |
pushremote a fix |
every target, description "a fix" |
Each target owns a git remote of the same name in the repository (the flat form
uses public, as before), so git push hub keeps working outside mgsh. A
target that fails does not stop the others; with more than one target
pushremote prints an n/m remotes updated summary. remotes = … restricts
and orders the set, which is mostly useful in a project .mgshrc — see below.
pushremote authenticates with the token, creates the repository (named after
the current project) if it does not exist yet, adds the credential-free remote,
and pushes all branches and tags. New repositories are private unless
visibility = public; any words after the command (and after any @name) are
set as the repository description on creation.
The token is sent as a one-shot HTTP auth header: it is never written into the
repo's git config, and it reaches git through the environment rather than the
command line, so it does not show up in the process table. Because ~/.mgshrc
then holds a credential, mgsh creates it mode 600 and warns at startup if an
existing file is readable by others. 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. Settings are resolved in three steps, each overriding the one before:
~/.mgshrc -> <project>/.mgshrc -> 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. The global file must be complete on its own: mgsh has to work outside
any project, so a project file can only refine it, never complete it.
It uses simple key = value (or key: value) lines; a # preceded by
whitespace starts a comment, so a value may still contain a literal # (quote
the value to keep a trailing comment-like part). 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'
config shows the resolved result at any time.
Settings reference
Every setting can also be given as an environment variable named MGSH_<KEY>
(e.g. MGSH_GITHOST), which wins over both files. "Scope" says whether a
project .mgshrc may override the setting.
| setting | scope | meaning |
|---|---|---|
base |
global | directory holding the projects — required |
githost |
project | git server host — required |
gitport |
project | ssh port — required |
gituser |
project | ssh user on the git server — required |
gitpath |
project | path of the bare repositories on the server — required |
gitkey |
project | ssh identity for the git server: a bare name is looked up in ~/.ssh, a path is used as given. Applies to mgsh's own ssh calls and to git's ssh transport. Unset = normal ssh/~/.ssh/config behaviour |
gitname |
global | user.name written to the global git config at startup |
gitemail |
global | user.email written to the global git config |
pushdefault |
global | push.default written to the global git config |
editor |
project | opener used by open/view when the project has no Xcode workspace (default coda) |
remoteurl |
project | base URL of a single mirror server (target name public) |
remotekey |
project | API token for remoteurl |
remotetype |
project | gitea|github|gitlab; auto-detected from the URL when unset |
remotevisibility |
project | private (default) or public for repositories created by pushremote |
remote.<name>.url |
project | base URL of the named mirror target |
remote.<name>.key |
project | API token for that target |
remote.<name>.type |
project | provider override for that target |
remote.<name>.visibility |
project | visibility for that target |
remotes |
project | comma- or space-separated list restricting and ordering the mirror targets |
mirror |
project | truthy (1/true/yes/on) → every push also mirrors |
The three settings written to the global git config are applied at startup, and
only when they actually differ, so a plain mgsh status does not rewrite
~/.gitconfig.
Per-project configuration
A project may carry its own .mgshrc, which overrides the global settings while
that project is active — a project on a different git server, with a different
editor, or mirrored to a different place:
# ~/src/myproject/.mgshrc
githost = other.example.com
remotes = hub # mirror this project to github only
mirror = true
It is read when you cd into the project and applies to every command that
follows. base, gitname, gitemail and pushdefault stay global — base
defines where projects live, and the identity keys are written to your global
git config, which should not change as you move between projects. mgsh says so
when a project file sets one of them. Aliases also remain global; alias always
writes to ~/.mgshrc.
rescan re-reads both files, so an edit takes effect without restarting. A
reload that would leave the global config incomplete is refused, and a changed
base needs a restart — mgsh says so rather than half-applying it.
A project .mgshrc is a normal file in the working tree, and push commits
everything: keep API tokens out of it unless it is git-ignored. mgsh warns
when a project config holds a token that git is not ignoring — including before
init, since that first commit would publish it too.
See mgshrc.example for an annotated template.
Git server layout
mgsh expects nothing more than a user with ssh access whose login directory is
gitpath — every remote command runs there without a cd. Bare repositories
live directly in it, snapshots in an archive/ subdirectory:
/home/git/ <- gitpath, and the login directory of gituser
├── myproject.git <- created by `init`, cloned by `clone`
├── notes.git
└── archive/ <- created by hand; needed by `archive` and `clone -a`
└── myproject_170126.1432_before_rewrite.git.tar.gz
Setting this up once:
ssh git@git.example.com 'mkdir -p /home/git/archive'
Ownership does not matter — list reads whatever ls -ltr reports, whichever
user or group owns the repositories. Everything mgsh runs there is plain
ls/mkdir/cp/tar/rm plus git --bare init, with all names shell-quoted,
so a restricted shell that allows those commands is enough.
archive copies the server-side repository (not the working tree) to
archive/<project>_DDMMYY.HHMM[_comment].git.tar.gz; clone -a <name> unpacks
one and clones it back. list -a lists them.