v1.5.0
gwm is no longer a GitHub-only tool. Issue and pull/merge request lookups
now go through a Forge trait with two implementations
(#419): the existing GitHub one
(gh) and a new GitLab one (glab). Everything a worktree actually is stays
forge-neutral: the branch.<name>.gwm-* link storage, bootstrap, branch
naming, the daemon and the TUI do not know which forge is in play, so nothing
about a repo becomes forge-specific on disk.
Most of the work in this line is not the happy path; it is what the review loop
found underneath it. Making non-github.com hosts reachable for the first time
turned a hardcoded assumption into a decision that had to be made explicitly:
an unrecognised host is not assumed to be GitHub, because assuming it would
have sent an authenticated call, and whatever token the environment carries, to
whatever host a cloned repo’s origin happened to name. Authorising a
self-hosted host is therefore its own decision ([forge_hosts] in your own
global config, or the TOFU trust ledger via the new gwm trust add), kept
deliberately apart from the forge key that only names a backend. The same
pass pinned which instance each CLI call reaches, stripped inherited repository
selectors from the child process, and made an unrecognised CI state report as
unknown rather than as green.
One caveat stated plainly, and one clarification of what it does not mean. The
GitLab backend was exercised end to end against a local fake GitLab server
driven by the real glab 1.109.0 binary: pagination (glab api --paginate
concatenates arrays rather than merging them, unlike gh), the /-/ URL
shapes, host and port pinning, and the Private-Token header a pinned
GITLAB_HOST receives were all measured rather than assumed, and the
environment-variable surface was audited against glab’s own source. What has
not happened is a run against a real GitLab instance, self-hosted
especially; a mock proves the client, not the server. Field reports are
welcome.
- Multi-forge support: a
Forgetrait and a GitLab (glab) backend (#419). Issue / PR lookups now go through aForgeabstraction with two implementations: the existing GitHub one (gh) and a new GitLab one (glab). Worktrees, bootstrap, branch naming and thebranch.<name>.gwm-*link storage are unchanged and forge-neutral - only the network layer knows which forge is in play.- New
forge = "github" | "gitlab"key in.gwm.toml. Omitted, the forge is inferred from theoriginhost; a self-hosted instance lives on an arbitrary domain and cannot be detected from the URL, so the explicit key is how you name the backend, and it always wins over inference. - New
[forge_hosts]table, read from your own~/.config/gwm/config.tomlonly -"host" = "github" | "gitlab". This is what authorises gwm to make an authenticated call against a self-hosted host, and it is deliberately separate fromforge: that key says which backend you run, never which hosts may receive your token. Keyed per host so one config can describe a mixed fleet (a self-hosted GitLab and a GitHub Enterprise), which a singleforgekey cannot. Matching is case-insensitive. - A host gwm does not recognise is not assumed to be GitHub. Only the
vendors’ own domains resolve without configuration -
github.com,ghe.com,gitlab.com; agitlab.*hostname is chosen by whoever owns the domain and states nothing. Anything else needs theforgekey, and gwm reports that rather than guessing. Guessing would have sent an authenticatedghcall - and a$GH_ENTERPRISE_TOKEN- to whatever host a cloned repo’soriginhappened to name. - On such a host, authorisation comes from
[forge_hosts]above, or from a repo whose.gwm.tomlyou have approved in the TOFU trust ledger. That file ships with the repo, so letting it name the host on its own would have handed a hostile clone an authenticated call to its own server from a plaingwm status- with whatever token the environment carries. Verified rather than assumed: givenGITLAB_HOST,glab1.109.0 sends the ambientGITLAB_TOKENto whatever it names, as aPrivate-Tokenheader, with no host scoping of its own. Same ledger as[[bootstrap.command]], checked non-interactively because the TUI’s selection path cannot prompt -gwm trust addis how you answer, andGWM_ALLOW_BOOTSTRAP=1remains the CI escape hatch (it bypasses this gate too, being the same decision about the same file). $GWM_GLABoverrides theglabbinary, mirroring$GWM_GH.- New
gwm trust add: approve the current repo’s.gwm.tomlwithout running anything. The existing prompt only fires when the file has a bootstrap surface to execute, so a config that only namesforgecould never be approved through it. gwm doctorprobes the forge CLI, but only whenforgeis set explicitly, so repos that never opt in gain no new warning.- GitLab specifics absorbed at the parse boundary:
iid(notid) as the user-visible number,opened/lockedstates, nested subgroup paths,#RRGGBBlabel colours,due_datevsdue_on, andstate_eventfor milestone transitions.
- New
Changed
Section titled “Changed”- Repo-slug extraction is host-agnostic
(#419).
originURLs are now parsed into host + path for any host, in both scp-like (git@host:path) and scheme-ful (ssh://,https://) forms. Pre-#419 a non-github.comremote was rejected outright, which made a GitLab remote unusable before the backend got a say. Issue / PR URLs are built from the parsed host instead of a hardcodedhttps://github.com/, so self-hosted instances get links to their own server. - An unrecognised CI state is reported as unknown, never as green
(#419).
CheckOutcomegained an explicitUnknownvariant, rendered as its own row in the CI checks overlay. It aggregates as non-green, so a forge state this build does not know can no longer paint a passing CI that is not passing.
- The trust ledger keys on the repo again, not on its host
(#463). The ledger stores
(origin, sha256(.gwm.toml)), and theoriginhalf had come to be built two different ways: the bootstrap gates used the fulloriginremote URL, while the forge host gate andgwm trust add(both new in #419) usedRemoteRef::web_origin- scheme + host only. That degraded the key from “this repo” to “this host”, so approval was shared by every repo on that host whose.gwm.tomlhashed identically. Since that file is normally a template copied across a team’s repos, identical hashes are the ordinary case rather than a corner case, and a hostile repo could inherit a sibling’s approval by shipping its config verbatim. The two gates also stopped seeing each other’s entries, sogwm trust addprinted✓ trusted …andgwm createin the same repo still refused. All four call sites now go through onetrust::origin_key_for_repohelper, so the key cannot drift again. Ledger entries written before this fix by the bootstrap path are unaffected. - A blocking
manualGitLab pipeline no longer reads as green (#419). It was mapped to passing by analogy with GitHub’sSKIPPED, but a pipeline reportsmanualwhile it waits on a blocking manual job - suspended, and possibly barring the merge. - Generated URLs keep the remote’s scheme and web port
(#419).
http://host:8080/…was collapsed to the bare host and rebuilt ashttps://host/…, sogwm openproduced dead links for self-hosted instances. Anssh://host:2222port is still dropped - that one addresses sshd, not the web UI. $GITLAB_HOSTis pinned on everyglabcall (#419).glabotherwise resolves the instance from the process working directory, which in workspace mode is the workspace root rather than the row’s repo - a same-named project on another instance could be read and its iid persisted locally.- Issue / MR bodies are redacted from Command Logs
(#419).
glabhas no--body-file, so the rendered body rides in--description; the transcript now stores its length instead of its contents. - A milestone
due_oncarrying a time is refused on GitLab (#419). GitLab’sdue_dateis date-only, so such a value could never converge and was rewritten on every push. It now fails with the cause named. $GH_HOSTis pinned for a GitHub Enterprise origin (#419). Host-agnostic slug parsing made non-github.com hosts reachable for the first time, and without the pinghsilently targeted github.com and could read a same-named repo on another tenant.- A guessed origin never overrides the forge CLI’s own configuration
(#419). An SSH remote carries
no web scheme or port, so
https://<ssh-host>is a guess: good enough to build a link, not good enough to force through$GITLAB_HOST/$GH_HOSTover aglab/ghsetup that may name a different web hostname. Same for the no-origin creation fallback, which briefly forced gitlab.com. - Clearing a label or milestone field in
.gwm.tomlclears it upstream (#419). Dropping adescriptionordue_onproduced an update that omitted the field entirely, so the remote value survived and the same update replayed on every push. The declared set is the desired state, so absent optionals are now sent empty on the GitLab update path. - A malformed
.gwm.tomlno longer silently picks the wrong forge (#419). The forge lookups added here swallowed config errors and fell back to host inference, dropping aforge = "gitlab"a self-hosted instance depends on. Single-repo paths now surface the error; a workspace row with a broken config skips detection instead of guessing. - The forge CLI runs inside the repo, not gwm’s working directory
(#419).
gh/glabresolve the instance from their cwd when nothing pins it; gwm’s cwd is the workspace root, not the row’s repo. This is the root fix for the wrong-tenant hazard and covers SSH remotes, where no host can honestly be pinned.$GH_HOSTis now pinned for github.com too, since an ambientGH_HOSTwould otherwise retarget a github.com repo. - The forge CLI host pin carries the port
(#419).
https://ghe.example:8443/…pinned onlyghe.example, sendingghto port 443 - guaranteed wrong, and possibly a different instance listening there. - IPv6 remotes parse correctly
(#419).
git@[::1]:group/repo.gitwas split on the first colon, yielding hostgit@[and a nonsense path. The scp-form and port splits are now bracket-aware. - Inherited repository selectors are stripped from the forge CLI
(#419).
$GH_REPO,$GITLAB_REPO,$REMOTE_ALIASand$GIT_REMOTE_URL_VAReach override which project the CLI acts on and are inherited from gwm’s own environment, so an exported one silently retargeted every call. Host variables are deliberately left alone - gwm does not always know the host. - Both forges’ alternate SSH endpoints map back to the API host
(#419).
ssh://git@ssh.github.com:443/…andaltssh.gitlab.comexist for networks that block port 22; the API and web UI stay on the canonical domain, so pinning the SSH host broke every call and produced dead links. - PR / MR auto-detection ignores a fork sharing the branch name
(#419).
--head/--source-branchmatch the branch name only, so a fork’s PR could win and be persisted as this branch’s detected PR. Filtered on GitHub’sisCrossRepositoryand on GitLab’ssource_project_idvsproject_id. - An SSH origin lets
glabresolve the project itself (#419). No host can honestly be pinned from an SSH remote, but passing--repo <slug>anyway made glab resolve it against its default host - defeating the working directory gwm sets. The flag is dropped in that case, and the REST paths useglab api’s:fullpathplaceholder. - Issue / MR links come from the forge when the local URL would be a guess
(#419).
https://<ssh-host>/…is wrong whenever the SSH hostname is not the web one, or the web UI runs on HTTP or a non-standard port.gwm opennow asks for the server’sweb_url; the TUI reuses an already-cached status so the render thread issues no request, and both fall back to the constructed URL offline. - Hook
{owner}/{repo}placeholders split a nested namespace correctly (#419). A GitLab slug can begroup/sub/proj; splitting on the first separator gaveowner=group,repo=sub/proj. The namespace is everything before the last one. - The GitHub host is pinned whenever the slug is known
(#419), including github.com
and including SSH origins.
ghtakes no hostname from--repo owner/repo, bakes the slug intogh api repos/<slug>, and does not fall back to the working directory the wayglabdoes - so an ambientGH_HOSTretargeted every call, and pinning nothing on an enterprise host meant silently querying github.com. gwm doctorhonours$GWM_GH/$GWM_GLAB(#419). The forge-CLI probe looked for the baregh/glabname, warning about a working setup that points at an alternative binary.- Ancestor group labels stay out of the project label diff
(#419). GitLab returns them
from the project endpoint by default, so
gwm labels push --pruneproposed deleting labels the project does not own.
- New GitLab (multi-forge) integration
page covering forge selection, nested groups, the pipeline-to-CI-state
mapping, and the deferred TUI terminology sweep
(#419), with its French
mirror at
docs/fr/5.integrations/5.gitlab.md. - Roadmaps (
ROADMAP.md,docs/7.roadmap.mdEN / FR) ported to the v1.5.0 line, and the packaged-channel version notes refreshed.