gitq import
Rebuilds the local store from the forge's open merge requests, replacing whatever is tracked locally.
Usage
gitq import [--replace]
import takes no --stack: it replaces the whole store, every tracked stack at once (see Global flags).
Flags
| flag | meaning |
|---|---|
--replace | Overwrite a non-empty local store instead of refusing. |
Behavior
Fetches the open MRs of this project only, walks targetBranch chains to discover stack-shaped trees, and replaces the tracked stacks with what it finds (ForgeSync.importFromForge, src/core/forge-sync.ts:297-347). The project is the one your origin remote points at, and an MR is kept only when its own web URL resolves to that same project (filterPRsToProject, src/core/forge-helpers.ts:188-196), so a branch of the same name in another project on the same forge instance can no longer be spliced into a discovered stack. Host and path are matched together (sameProject, src/core/forge-helpers.ts:68-71), so the same group/project on a different forge host is a different project, and an MR whose web URL is not of a known MR/PR shape is placed nowhere and dropped. It is the reverse of publish: forge state becomes the source of truth instead of the local store.
Chains are walked one author at a time, as well as one project at a time (discoverStacksFromPRs, src/core/forge-helpers.ts:198-244). A target branch is shared state: two people both stacking on the same intermediate branch are adjacent in the target-branch graph without being in the same stack, and grouping on adjacency alone spliced them into one chain neither was working on. The trade is deliberate and worth knowing before you rely on this: a stack that genuinely spans two authors, where you built on a colleague's branch, is reported as two stacks. Authorship is the only signal the forge offers here short of an explicit marker, so import prefers a wrong split to a wrong join. An MR the forge named no author for joins no chain at all.
The guards run in this order, and the second one is checked before the token, on purpose, so the refusal works offline:
- Any lease active anywhere in the repo, running or parked, for any stack, refuses:
cascades are active; finish or abort them first(src/cli/commands/forge.ts:144-147). Unlike every other mutating command's per-stack lease guard, this one fires for any lease, becauseimportis about to discard the store those leases are tracked against. - A non-empty local store, without
--replace, refuses before the token or the network are touched:import would discard <N> locally tracked stack(s) and re-mint stack ids; pass --replace to overwrite the local store(src/cli/commands/forge.ts:152-159). - Only then is the forge provider built, where the remote's host decides both the forge and the credential, and where three refusals happen: an unreadable host (
no forge host in remote "..."), a host naming no forge gitq knows (cannot tell which forge "..." is), and a missing token (no gitlab token for gitlab.com (set GITLAB_TOKEN or track the repo with rt (rt daemon track <repo> live branches); <repoPath> is not registered with rt (~/.mattstack/rt/repos.json))for a repo not tracked with rt), all increateForgeProvider(src/cli/provider.ts:44-78). See Configuration. - A remote no project path can be read from refuses rather than importing whatever the token can see:
cannot read a project path from remote "<url>"; import keeps only the MRs of the project the remote points at(src/core/forge-sync.ts:298-303). Scoping is the whole promise of this command, so a remote that names no project is an error here, unlike in the read-only discovery calls that share the same fetch.
A scope that matched nothing says so. When the forge returned open MRs and none of them belong to this project, import writes a warning to stderr naming the project and the remote it was read from: none of the <N> open MR(s) GitLab returned belong to <group/project> (read from remote <url>); if the project was renamed or transferred, update the remote and import again (src/cli/commands/forge.ts:176-185). Without it, a stale remote after a rename or transfer is indistinguishable from a forge with nothing on it: both print imported 0 stack(s), and with --replace the store that was there is already gone. The exit code stays 0 either way, and --json still emits { store } on stdout.
Re-mints every stack id, every time. Each stack import creates gets a fresh crypto.randomUUID() (StackManager.createStack, src/core/stack-manager.ts:42-44), with no relationship to whatever id the same-looking stack had before, even importing back the exact MRs just published. The stack name shown in gitq stacks is a readable slug derived from the tip branch's MR title (deriveStackId, src/core/forge-sync.ts:761-791). That name is stable across two imports of the same MRs: the tip is chosen from the sorted leaves and the discovered stacks are sorted before naming, so neither the leaf pick nor a -2 dedup suffix follows the order the forge listed things in. The underlying id never is stable, though. Anything keyed to the old id, most importantly any gitq undo history for operations logged before the import, stops resolving to a current stack the moment --replace runs.
With --replace and no leases active, the whole store is replaced, not merged with anything written concurrently (updateStore(ctx.repoRoot, () => store), src/cli/commands/forge.ts:169-174). Prints imported <N> stack(s) and exits 0.
Exit codes
0: the store was replaced.1: any lease active anywhere in the repo, a non-empty store without--replace, no forge token, or a remote no project path can be read from.
Never exits 2.
See also
- Publish a stack: the full refusal rules and the stack-id warning, verified against source.
- Recover from a mistake: when to reach for
import --replaceinstead ofgitq sync. gitq publish: the direction this command reverses.