`open` and `view` shared one implementation and differed in a single line: `open` also made the project the active one. Only `view` is left, with the behaviour it always had. Dropping it from builtinCmds is the part worth noting: a reserved word cannot be shadowed by an alias, so `open` is now free for one -- `alias open '!xdg-open $1'` works, which it could not before. That also made a completion test wrong, since it used `open` as its example of a name a builtin owns; it uses `status` now. The `runInDir(d, "open", ...)` calls stay: those are macOS's open(1), which is how an Xcode workspace gets opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
511 lines
22 KiB
Markdown
511 lines
22 KiB
Markdown
# 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](#build) · [Usage](#usage) · [Commands](#commands) · [Aliases](#aliases)
|
||
- [Overview](#overview) · [Credential check](#credential-check)
|
||
- [Public mirror (`pushremote`)](#public-mirror-pushremote) ·
|
||
[Releases](#releases)
|
||
- [Configuration](#configuration) · [Settings reference](#settings-reference) ·
|
||
[Per-project configuration](#per-project-configuration)
|
||
- [Git server layout](#git-server-layout)
|
||
|
||
## Build
|
||
|
||
```sh
|
||
./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`, `release`, `list`, `tag`, `archive`, `show`, `view`,
|
||
`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`/`view`, server repos for `clone`/`show`, branches/tags for
|
||
`checkout`/`tag`, mirror targets for `pushremote`/`release`, filesystem paths for
|
||
`dist`, and shell-style completion after `!` and for aliases that expand to
|
||
one), 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
|
||
```
|
||
|
||
It runs in the active project's directory. Tab completion works there the way it
|
||
does in a shell: the word after the `!` completes against the executables on
|
||
`PATH`, everything after it against the filesystem — relative to the project,
|
||
with `~/` and absolute paths understood, and directories completing with their
|
||
trailing slash so the next Tab walks into them. Dot entries stay out of the way
|
||
until the prefix asks for one.
|
||
|
||
```
|
||
< src/myproject > !vi ma<Tab> -> !vi main
|
||
< src/myproject > !vi <Tab> -> Makefile main.go main_test.go src/
|
||
< src/myproject > !gre<Tab> -> grep gresource
|
||
```
|
||
|
||
An alias that expands to a shell escape completes the same way, because its
|
||
arguments end up as shell arguments:
|
||
|
||
```
|
||
alias ll '!ls -la'
|
||
< src/myproject > ll ma<Tab> -> ll main
|
||
```
|
||
|
||
Only the alias's arguments complete, never its first word — the command is
|
||
fixed by the alias body. An alias to a builtin (`alias co 'checkout $1'`) is not
|
||
a shell line and is left alone.
|
||
|
||
Word splitting for completion is by whitespace only; quotes and backslash
|
||
escapes are left to the shell that runs the line.
|
||
|
||
### Commands
|
||
|
||
Run `help` for the full list. Highlights:
|
||
|
||
| command | description |
|
||
|---------------------------|------------------------------------------------|
|
||
| `cd [project]` | change project (no argument: back to the base) |
|
||
| `view [project]` | open a project in Xcode / the configured editor |
|
||
| `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` | inventory of all projects, local and on the server |
|
||
| `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 (`-a`: archives, with sizes) |
|
||
| `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 (`tag add v1.0 "why"` annotates) |
|
||
| `release [@name] <tag> [notes]` | tag and publish a release on the mirrors |
|
||
| `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.
|
||
|
||
### Listing the server
|
||
|
||
`list` shows what is on the git server, name first and aligned, ordered by
|
||
modification time — `push` touches the bare repository, so the most recently
|
||
worked-on project sits closest to the prompt:
|
||
|
||
```
|
||
< src > list
|
||
Betaflight3.0.0 Sep 28 2016 181M
|
||
website Mar 3 2024 2.1M
|
||
notes Jan 3 14:32 876K
|
||
3 repositories · 184M
|
||
```
|
||
|
||
The size is the repository's real disk usage on the server, asked of `du` in
|
||
the same round trip as the listing — a long listing reports the inode size for
|
||
a directory, which is the same number for every repository and says nothing. If
|
||
the server produces no usable sizes the column is left out rather than filled
|
||
with zeroes.
|
||
|
||
`list -a` lists the archives instead, whose sizes come from the listing itself;
|
||
a pattern filters by name (`list note`).
|
||
|
||
### Overview
|
||
|
||
`overview` (or `status -a`) is the one view that needs mgsh: it is the only
|
||
thing that sees the local base directory *and* the git server at once.
|
||
|
||
```
|
||
< src > overview
|
||
notes * ↑2 laptop 3h
|
||
website * ✓ desktop 2d → hub
|
||
Betaflight3.0.0 ✓ workstation 20d → gitea hub
|
||
experiments init
|
||
sandbox (wip) – laptop 1h init
|
||
4 projects · 2 dirty · 2 in sync · 2 to init
|
||
```
|
||
|
||
Every field sits in its own column, so the eye can go down one instead of
|
||
hunting along each line. The projects that need something done come first, the
|
||
settled ones next, and the directories the git server does not have yet come
|
||
last — those are a different kind of task. Within each group the order stays
|
||
alphabetical, so positions do not jump around.
|
||
|
||
| column | meaning |
|
||
|---|---|
|
||
| name | the project, with its branch appended when it is not `master`/`main` |
|
||
| `*` | uncommitted changes |
|
||
| `↑n` `↓n` | commits ahead of / behind the upstream (`↑2↓1` when both) |
|
||
| `✓` | in sync with the upstream |
|
||
| `–` | the branch tracks nothing — never pushed |
|
||
| `init` | the git server does not have this one; run `init` |
|
||
| host, age | who last committed and when, from the `[user@host]` stamp |
|
||
| `→` | mirror targets this repository has a remote for |
|
||
|
||
Every directory under the base gets a row, including those that are not
|
||
repositories at all — `init` is exactly what turns one into a project, so it
|
||
belongs in the table rather than in a list underneath it. Such a row simply has
|
||
no git state to show.
|
||
|
||
The host and age come from the commit itself — `push` writes `[user@host]` into
|
||
every message, so `overview` can say where a project was last worked on without
|
||
storing anything. On a setup spanning a laptop and a workstation that is usually
|
||
the piece of information you actually wanted.
|
||
|
||
The `init` column is the join no git command can do. If the server cannot be
|
||
reached, no row is marked and mgsh says so — not knowing is not the same as
|
||
knowing they are missing. The other direction, repositories on the server that
|
||
are not here, is what `list` shows.
|
||
|
||
### Credential check
|
||
|
||
`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. That is
|
||
the only action in mgsh that cannot be undone: a deleted server repository comes
|
||
back from an archive, a published credential does not.
|
||
|
||
So before anything is committed, the staged diff is checked for private keys,
|
||
GitHub/GitLab/Slack/AWS/PyPI tokens and credential-shaped assignments:
|
||
|
||
```
|
||
< src/notes > push new notes
|
||
2 possible credential(s) in what is about to be committed:
|
||
.env:3 credential assignment
|
||
API_KEY="<the offending line is shown in full here>"
|
||
deploy_key:1 private key
|
||
<the BEGIN … PRIVATE KEY header is shown here>
|
||
(set 'secretscan = off' to skip this check)
|
||
push anyway? y/N ?
|
||
```
|
||
|
||
Declining stops the push with nothing committed; the changes stay staged, so
|
||
`git restore --staged <file>` and a `.gitignore` entry are all it takes.
|
||
|
||
For a line that only *looks* like a credential and is meant to stay, put
|
||
`mgsh:allow` in it — a comment on that line is enough. That is better than
|
||
turning the whole check off for one false positive.
|
||
|
||
This is not a complete secret scanner and does not try to be one. It aims for a
|
||
high hit rate on what actually leaks, with few enough false alarms that the
|
||
prompt still means something: values that are plainly environment references,
|
||
constants, template slots (`<token>`, `${VAR}`) or masked stand-ins are ignored —
|
||
a test checks that mgsh's own README and `mgshrc.example`, both full of
|
||
credential-shaped text, stay quiet. Switch it off with `secretscan = off`.
|
||
|
||
### 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. Each server is one `remote.<name>.<field>` block:
|
||
|
||
```ini
|
||
remote.gitea.url = https://git.example.com
|
||
remote.gitea.key = <personal-access-token>
|
||
remote.gitea.type = gitea # optional; auto-detected from the url
|
||
remote.gitea.visibility = private # or public (default private)
|
||
|
||
remote.gitlab.url = https://gitlab.example.com
|
||
remote.gitlab.key = <personal-access-token>
|
||
remote.gitlab.type = gitlab
|
||
remote.gitlab.visibility = public
|
||
|
||
# remotes = gitea, gitlab # optional: restrict and order the set
|
||
# mirror = true # `push` also mirrors via pushremote
|
||
```
|
||
|
||
`<name>` is yours to pick; there is no other spelling. Older versions had a flat
|
||
`remoteurl`/`remotekey` pair for a single server — mgsh converts those to
|
||
`remote.public.*` in place on the next start and says so, keeping the git remote
|
||
name those versions used.
|
||
|
||
| command | pushes to |
|
||
|------------------------|-----------------------------------------------|
|
||
| `pushremote` | every configured target, in order |
|
||
| `pushremote @gitea` | only `gitea` |
|
||
| `pushremote @gitea @gitlab` | those two |
|
||
| `pushremote a fix` | every target, description "a fix" |
|
||
|
||
Each target owns a git remote of the same name in the repository, so
|
||
`git push gitlab` 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 the url
|
||
(`github.com` → GitHub, `gitlab*` → GitLab, otherwise Gitea) and can be forced
|
||
with `remote.<name>.type`. Set `mirror = true` to have every `push` mirror
|
||
automatically.
|
||
|
||
### Releases
|
||
|
||
`release` turns a commit into a published release on the mirror servers, in one
|
||
step — creating the tag, getting it onto the internal server, and then onto each
|
||
mirror as a release object:
|
||
|
||
```
|
||
release [@name ...] <tag> [notes]
|
||
```
|
||
|
||
```
|
||
< src/mgsh > release v4.1.0 first public build
|
||
remote hub released https://github.com/mike/mgsh.git
|
||
```
|
||
|
||
Without `@name` it releases to every configured mirror target, exactly like
|
||
`pushremote`. Everything after the tag becomes the release notes *and* the tag's
|
||
annotation.
|
||
|
||
**Notes are generated when you do not write any**: the tag's own annotation if
|
||
it has a real one, otherwise the commit subjects since the previous tag
|
||
(`- <subject>` per line, at most 50). So a plain `release v4.1.0` already
|
||
produces a usable changelog.
|
||
|
||
A tag ending in `-rc`, `-alpha`, `-beta` or `-pre` (optionally with digits) is
|
||
marked as a **pre-release** on Gitea and GitHub; GitLab has no such flag.
|
||
|
||
Releasing the same tag twice updates the existing release rather than failing.
|
||
But a tag that already exists **on a different commit** stops the command —
|
||
moving a published tag is how one version quietly comes to mean different things
|
||
on different servers. `release` also refuses when the repository is not on the
|
||
mirror yet and tells you to run `pushremote` first, rather than creating it as a
|
||
side effect.
|
||
|
||
**No binary assets.** The three providers handle uploads in three incompatible
|
||
ways — Gitea attaches them to the release, GitHub uses a separate upload host,
|
||
and GitLab does not host them at all but expects a link into its package
|
||
registry. mgsh publishes source releases with notes; if you need binaries,
|
||
upload them with the provider's own tooling.
|
||
|
||
## 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:
|
||
|
||
```ini
|
||
# --- 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 `view`
|
||
|
||
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; a mirror field is
|
||
`MGSH_REMOTE_<NAME>_<FIELD>`, so `MGSH_REMOTE_GITLAB_KEY` sets
|
||
`remote.gitlab.key`. "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 `view` when the project has no Xcode workspace (default `coda`) |
|
||
| `remote.<name>.url` | project | base URL of the mirror target `<name>` |
|
||
| `remote.<name>.key` | project | API token for that target |
|
||
| `remote.<name>.type` | project | `gitea`\|`github`\|`gitlab`; auto-detected from the url when unset |
|
||
| `remote.<name>.visibility` | project | `private` (default) or `public` for repositories `pushremote` creates |
|
||
| `remotes` | project | comma- or space-separated list restricting and ordering the mirror targets |
|
||
| `mirror` | project | truthy (`1`/`true`/`yes`/`on`) → every `push` also mirrors |
|
||
| `secretscan` | project | `off` disables the credential check `push` runs before committing (on by default; only an explicit `off` disables it) |
|
||
|
||
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/<project>_DDMMYY.HHMM[_comment].git.tar.gz`; `clone -a <name>` unpacks
|
||
one and clones it back. `list -a` lists them.
|