diff --git a/README.md b/README.md
index 3a0b7ed..64fe00b 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,15 @@
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`).
+directory. Go port of the original Perl `mgsh` (`mgsh.perl`).
+
+## Contents
+
+- [Build](#build) · [Usage](#usage) · [Commands](#commands) · [Aliases](#aliases)
+- [Public mirror (`pushremote`)](#public-mirror-pushremote)
+- [Configuration](#configuration) · [Settings reference](#settings-reference) ·
+ [Per-project configuration](#per-project-configuration)
+- [Git server layout](#git-server-layout)
## Build
@@ -24,6 +31,17 @@ Run the tests with `go test ./...`.
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 `/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 ` 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:
@@ -33,8 +51,11 @@ project, its git branch and a `*` dirty marker:
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.
+`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.
@@ -53,7 +74,7 @@ Run `help` for the full list. Highlights:
| command | description |
|---------------------------|------------------------------------------------|
-| `cd [project]` | change project |
+| `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 |
@@ -69,9 +90,39 @@ Run `help` for the full list. Highlights:
| `tag [add/checkout/delete]` | manage tags |
| `alias [name [cmd]]` | list, show or define a command alias |
| `unalias ` | remove a command alias |
-| `rescan` | refresh the cached server repository list |
+| `config [-k]` | show the effective configuration and its sources |
+| `rescan` | reload the config, refresh the cached repo list |
| `!` | run `` 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 ''` defines a reusable shortcut, persisted to
@@ -99,8 +150,8 @@ alias ec '!echo $1' # ec hello -> echo hello (shell)
### 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`:
+one or more public hosting servers (Gitea, GitHub or GitLab) over their REST
+API. A single server is configured flat:
```ini
remoteurl = https://git.example.com # base URL of the server
@@ -110,26 +161,64 @@ remotekey = # API token
# mirror = true # `push` also mirrors via pushremote
```
+Several servers get one `remote..*` block each:
+
+```ini
+remote.gitea.url = https://git.example.com
+remote.gitea.key =
+
+remote.hub.url = https://github.com
+remote.hub.key =
+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 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 `) 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` →
+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**. 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.
+mgsh has **no built-in defaults**. Settings are resolved in three steps, each
+overriding the one before:
-It uses simple `key = value` (or `key: value`) lines (`#` comments allowed);
-alias definitions live in the same file:
+```
+~/.mgshrc -> /.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:
```ini
# --- required ---
@@ -148,9 +237,97 @@ 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`.
+`config` shows the resolved result at any time.
+
+### Settings reference
+
+Every setting can also be given as an environment variable named `MGSH_`
+(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..url` | project | base URL of the named mirror target |
+| `remote..key` | project | API token for that target |
+| `remote..type` | project | provider override for that target |
+| `remote..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:
+
+```ini
+# ~/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:
+
+```sh
+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/_DDMMYY.HHMM[_comment].git.tar.gz`; `clone -a ` unpacks
+one and clones it back. `list -a` lists them.
diff --git a/mgshrc.example b/mgshrc.example
index 71ad053..165cea7 100644
--- a/mgshrc.example
+++ b/mgshrc.example
@@ -2,9 +2,11 @@
# in the required settings (mgsh refuses to start until they are set). This file
# is just an annotated reference.
#
-# Simple "key = value" (or "key: value") lines; '#' starts a comment. ~/.mgshrc
-# is the sole source of configuration (there are no built-in defaults); MGSH_*
-# environment variables override individual settings.
+# Simple "key = value" (or "key: value") lines; a '#' after whitespace starts a
+# comment. There are no built-in defaults. Settings are resolved in this order,
+# each step overriding the one before:
+#
+# ~/.mgshrc -> /.mgshrc -> MGSH_* environment variables
# --- required ---
base = /Users/me/src
@@ -20,13 +22,37 @@ gitpath = /home/git
# pushdefault = matching
# editor = code
-# --- pushremote: mirror to a public server (gitea/github/gitlab) via its API ---
+# --- pushremote: mirror to public servers (gitea/github/gitlab) via their API ---
+# A single server, the flat form (this target is named "public"):
# remoteurl = https://git.example.com
# remotekey =
-# remotetype = gitea # optional; auto-detected from remoteurl
+# remotetype = gitea # optional; auto-detected from remoteurl
# remotevisibility = private # visibility of created repos (default private)
+#
+# Or any number of named servers. `pushremote` pushes to all of them in the
+# order given, `pushremote @hub` to a single one. Each target gets a git remote
+# of the same name in the repository.
+# remote.gitea.url = https://git.example.com
+# remote.gitea.key =
+# remote.hub.url = https://github.com
+# remote.hub.key =
+# remote.hub.type = github # optional; auto-detected from the url
+# remote.hub.visibility = public # default private
+# remotes = gitea, hub # optional: restrict and order the set
+#
# mirror = true # `push` also mirrors via pushremote
+# --- per-project overrides ---
+# A /.mgshrc overrides all of the above for that project only, except
+# base, gitname, gitemail and pushdefault, which stay global. Typical use:
+#
+# # ~/src/myproject/.mgshrc
+# remotes = hub # this project is mirrored to github only
+# mirror = true
+#
+# Keep tokens out of a project .mgshrc unless it is git-ignored — mgsh warns
+# when it is not.
+
# Aliases live in the same file (managed by the `alias` command). The expansion
# is a mgsh command line; $1..$N and $*/$@ expand arguments, and a shell command
# needs a leading '!'.
diff --git a/version.txt b/version.txt
index 30b26df..f05f0cb 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-4.0.11
+4.0.23