Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

push

Push a feature branch to the remote, together with the branches it is stacked on. Automatically detects the remote type and uses the appropriate push strategy.

Usage

git loom push [branch] [--no-pr] [-f|--force]

Arguments

ArgumentDescription
[branch]Branch name or short ID (optional; interactive picker if omitted)

Flags

FlagDescription
--no-prPush without creating a PR or Gerrit review (see below)
-f, --forcePush with --force instead of --force-with-lease --force-if-includes

Stacked Branches

A branch is stacked on another when it is built on top of it — its oldest commit’s parent is the other branch’s tip. That is exactly what git loom status draws with │├─ between two branches. Stacks can be several branches deep.

Pushing a stacked branch pushes what its review depends on. With d on c on b on a:

git loom push b
# ✓ Pushed `a`, `b` to `origin`
#   Re-pushed above `b`: `c`
# ! Not pushed above `b`: `d`
#   Run `loom push d` to publish them
  • a, b — the branch and everything below it, always.
  • c — a branch above b whose remote is no longer at its tip, whether the rebase that rewrote b rewrote it too or it carries commits of its own. Either way the server no longer matches the stack, so it is re-pushed. No PR is created for it.
  • d — a branch above b that was never pushed is only mentioned. It has no remote ref to keep in step, and creating one is your call.

Everything goes out in one atomic git push (one lease check, and a refused branch leaves the others untouched). Branches hidden by loom.hideBranchPattern are never pushed: pushing one, or a branch stacked on one, is refused. Gerrit needs none of this: refs/for/ already uploads the whole chain of changes.

Pull requests per layer

Each pushed branch gets a PR that targets the branch below it; the bottom one targets your upstream branch (main):

RemoteWhat loom does
GitHubCreates missing PRs with --base <branch below>, retargets an existing PR whose base is wrong (PR retargeted to 'a': …), then links the PRs into a GitHub stack
GitLabPushes each branch with merge_request.target=<branch below>; GitLab shows the dependency and retargets on merge. A re-pushed upper branch keeps its existing MR and gets no new one
Azure DevOpsNot supported — pushing a stacked branch is refused (see Azure DevOps)
GerritUnchanged

On GitHub the stack shows a stack map on every PR, reviewers see one layer at a time, and when the bottom PR merges GitHub rebases and retargets the ones above by itself. Nothing is stored locally: loom reads each PR’s stack membership back through gh api on every push and creates or extends the stack as needed. In a stack, new PRs are created directly and their URLs printed instead of opening the browser, because the stack can only be linked once every PR exists.

git loom push b
# ✓ Pushed `a`, `b` to `origin`
# ✓ PR created: https://github.com/owner/repo/pull/41
# ✓ PR created: https://github.com/owner/repo/pull/42
# ✓ Stack #7 registered with 2 PRs

GitHub does not support stacks across forks: in a fork workflow every layer still gets its PR, but they all target the upstream branch and a warning says so. If the stack API is unavailable on your host, or a PR’s stack membership cannot be read, the PR bases are still set and loom points you at the gh-stack extension.

Creating a stack

A stack is just a shape of your branches:

  • git loom branch part-1 -t <commit inside feature> splits a branch into two stacked layers.
  • git loom branch b -t a followed by git loom commit -b b puts b’s first commit on top of a.

Amending any layer (fold, absorb, reword) rewrites the layers above it as well; the next push of any layer re-publishes them. Adding a commit to a lower layer with commit -b or fold <commit> <branch> is not supported yet — commit to the top layer or fold into an existing commit instead.

When the bottom PR merges, git loom update rebases your integration branch and the stack shrinks from the bottom.

Remote Type Detection

Detection priority (first match wins):

  1. Explicit configgit config loom.remote-type set to github, gitlab, azure, gerrit, or plain
  2. URL heuristics — remote URL contains github.com → GitHub
  3. URL heuristics — remote URL contains gitlab → GitLab
  4. URL heuristics — remote URL contains dev.azure.com → Azure DevOps
  5. Hook inspection.git/hooks/commit-msg contains “gerrit” → Gerrit
  6. Gerrit confirmation — if nothing matched but the remote URL uses Gerrit’s standard SSH port (:29418/) or recent commits carry a Change-Id: trailer, you are asked to confirm; the answer is saved as loom.remote-type (gerrit or plain) so you are only asked once
  7. Fallback — Plain Git

Self-hosted GitLab whose hostname does not contain gitlab (e.g. invent.kde.org) is not auto-detected — set git config loom.remote-type gitlab. Even without detection, a plain push still surfaces the MR link the server prints.

Push Remote Selection

Detection priority (first match wins):

  1. Explicit configgit config loom.push-remote <remote>
  2. GitHub fork convention — if the integration remote is named upstream and origin exists, push to origin
  3. Fallback — integration branch’s remote

For non-standard fork setups (e.g., integration branch tracks origin but you push to personal), set:

git config loom.push-remote personal

Push Strategies

Plain Git (default)

git push --force-with-lease --force-if-includes -u <remote> <branch>

Uses --force-with-lease because woven branches are frequently rebased. --force-if-includes adds extra safety.

When the remote rewrote your branch

Landing the pull request below a stacked branch makes the forge rebase that branch for you, server-side. The tip on the remote is then a commit your clone never held, and --force-if-includes refuses every later push of that branch — with a hint to git pull that does not help, because merging it back only adds content you already have.

loom names the flag that does:

✗ git push failed
  › If `feature-a` has diverged on the remote, push again with `loom push feature-a -f`

That is the push you ran, forced — --no-pr and the branch you named both survive it. loom does not check whether the remote really diverged, or whether what is there is yours: a colleague’s commit and a forge’s rebase look the same from your clone, and only you can tell them apart. Fetch and look if you are unsure. Typing the flag is what makes it your call.

Pass -f / --force to push with plain --force instead, for when the lease check refuses a push you know is correct. It applies to every remote type except a Gerrit refs/for/ review push, which never forces. It also applies to every branch the push contains, not only the one named: a stack goes out in a single git push, so forcing it overwrites the downstack and re-published upstack branches too.

Any remote: lines containing an http(s) URL are shown below the success message, so the MR/PR creation link that servers like GitLab print on push is visible even when the remote type was not detected.

GitHub

Pushes the branch with --force-with-lease, then checks whether a PR already exists for the branch:

  • PR exists — prints the PR URL (PR updated: https://github.com/owner/repo/pull/42) without opening the browser
  • No PR — creates the PR via gh pr create with an auto-generated title and description (see PR Title and Description below)

For a stacked branch, see Stacked Branches. If gh is not installed, the push succeeds with a message suggesting to install it.

In a fork workflow (tracking upstream/main), pushes go to origin (your fork) and the PR targets the upstream repository automatically.

If the branch being pushed is the upstream target branch itself, PR creation is skipped.

GitLab

git push --force-with-lease --force-if-includes \
    -o merge_request.create -o merge_request.target=<target> -u <remote> <branch>

Uses GitLab push options so the server creates a merge request (or points to the existing one) during the push. The MR URL GitLab prints is shown below the success message. No extra CLI tool is required. If the branch being pushed is the upstream target branch itself, the MR push options are skipped.

Azure DevOps

Azure has no stacked pull requests, and az repos pr update cannot retarget an existing one either, so a stack would land as PRs whose base says nothing a reviewer can rely on. Pushing a stacked branch is refused before anything reaches the remote:

✗ `b` is stacked on `a` — Azure DevOps has no stacked pull requests
  Land the branches below it first, or push without a PR (`--no-pr`)

--no-pr still pushes the whole stack; it is the pull requests Azure cannot express.

For a branch of its own, loom pushes it with --force-with-lease, then checks whether a PR already exists:

  • PR exists — prints the PR URL (PR updated: https://dev.azure.com/...) without opening the browser
  • No PR — creates the PR via az repos pr create with an auto-generated title and description (see PR Title and Description below)

The organization, project and repository are read from the remote URL and passed explicitly; --detect is only used when the URL cannot be parsed. If az is not installed, the push succeeds with a message suggesting to install it.

Gerrit

git push <remote> <branch>:refs/for/<target>

Uses the refs/for/ refspec. No topic is set. After pushing, any review URLs returned by Gerrit are extracted from the remote output and displayed below the success message.

PR Title and Description

When creating a new PR (GitHub or Azure DevOps), git-loom auto-generates the title and description from the commits the PR contains — those between its base and the branch tip. A stacked PR includes only its branch’s own commits. When a PR targets the trunk instead (in a fork, or because its lower layer was dropped as merged), it also includes commits from the lower layers, so the description always matches the diff a reviewer sees:

  • Single commit — the commit subject becomes the PR title and the commit body becomes the description.
  • Multiple commits — you are prompted for a PR title (the prompt names the branch). The description is built by concatenating all commit messages (oldest to newest), separated by --- dividers.
  • Empty branch — the branch name is used as the title with an empty description.

Pushing Without a PR or Review

Use --no-pr when you want to push a branch to the remote without triggering PR or review creation — for example, to back up a branch, share work-in-progress, or push to a staging ref.

Remote type--no-pr behavior
PlainSame as normal (force-with-lease push)
GitHubSkips gh pr create
GitLabPlain push without merge_request.create push options
Azure DevOpsSkips az repos pr create
GerritPlain push to branch ref instead of refs/for/ (see below)

Gerrit: wip/ prefix warning

In Gerrit, pushing directly to a branch ref (not refs/for/) creates a remote branch that requires a project admin to delete. To protect against accidental non-deletable branches, --no-pr on Gerrit prompts when the branch name doesn’t start with wip/:

? Branch `feature-a` is not prefixed with `wip/` — a Gerrit admin will be needed to delete the remote branch later
> Push as `feature-a` (admin required to delete it later)
  Push as `wip/feature-a` instead
  Cancel
  • Push as-is — pushes to remote/feature-a; an admin is needed to delete it later
  • Push as wip/<branch> — pushes with refspec feature-a:wip/feature-a; your local branch name is unchanged
  • Cancel — aborts the push

If the branch already starts with wip/, no prompt is shown.

Examples

Push to a plain remote

git loom push feature-a
# Pushed 'feature-a' to origin

Push to GitHub (new PR)

git loom push feature-a
# Pushed 'feature-a' to origin
# (browser opens to PR creation page)

Push to GitHub (PR already exists)

git loom push feature-a
# Pushed 'feature-a' to origin
# PR updated: https://github.com/owner/repo/pull/42

Push a stacked branch to GitHub

git loom push feature-b        # feature-b is stacked on feature-a
# Pushed `feature-a`, `feature-b` to `origin`
# PR updated: https://github.com/owner/repo/pull/41
# PR created: https://github.com/owner/repo/pull/42
# Stack #7 registered with 2 PRs

Push to Azure DevOps (new PR)

git loom push feature-a
# Pushed 'feature-a' to origin
# (browser opens to PR creation page)

Push to Azure DevOps (PR already exists)

git loom push feature-a
# Pushed 'feature-a' to origin
# PR updated: https://dev.azure.com/org/project/_git/repo/pullrequest/42

Push to Gerrit

git loom push feature-a
# Pushed 'feature-a' to origin (Gerrit: refs/for/main)
#   › https://gerrit.example.com/c/project/+/12345

Interactive selection

git loom push
# ? Select branch to push
# > feature-a
#   feature-b
# Pushed 'feature-a' to origin

Push without opening a PR (GitHub)

git loom push feature-a --no-pr
# Pushed 'feature-a' to origin

Push without a review, renaming to wip/ (Gerrit)

git loom push feature-a --no-pr
# ? Branch `feature-a` is not prefixed with `wip/`...
# > Push as `wip/feature-a` instead
# Pushed 'feature-a' to origin as 'wip/feature-a'

Override remote type

git config loom.remote-type gerrit
git loom push feature-a
# Pushed 'feature-a' to origin (Gerrit: refs/for/main)
#   › https://gerrit.example.com/c/project/+/12345

Prerequisites

  • Must be on an integration branch with upstream tracking
  • The target branch must be woven into the integration branch
  • Network access to the remote
  • gh CLI (optional, for GitHub PR creation)
  • az CLI (optional, for Azure DevOps PR creation)