`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>
`release [@name ...] <tag> [notes]` does the whole chain in one step —
create the annotated tag, push it to the internal server, then push it to
each selected mirror and turn it into a release object there. Target
selection reuses pushremote's @name mechanism, so the two behave alike.
Notes are generated when none are given: the tag's own annotation when it
carries more than the default, otherwise the commit subjects since the
previous tag, capped at 50 lines. `tag add v1.0 "why this exists"` now
takes a message, which is what that fallback reads; before, the
annotation was always just the tag name.
Tags ending in -rc/-alpha/-beta/-pre are marked as pre-releases on Gitea
and GitHub. Releasing the same tag twice updates the existing release;
a tag that already points at a different commit stops the command, since
moving a published tag makes one version mean different things per
server. A repository that is not on the mirror yet is reported instead of
being created as a side effect.
Binary assets are deliberately out of scope: Gitea attaches them to the
release, GitHub uses a separate upload host, and GitLab does not host
them at all but wants a link into its package registry.
The providers differ in path shape and field names -- GitLab addresses
projects by URL-encoded path, calls the notes "description" and has no
pre-release flag -- so this comes with a recording httptest stand-in that
asserts the exact requests for all three. That harness also covers
authUser, repoExists and the auth header forms, which had no test at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>