Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20260514-113901.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: 'integration: Add ''gs integration'' command for managing a combined integration branch that merges multiple stack tips onto trunk.'
time: 2026-05-14T11:39:01.837856-04:00
171 changes: 171 additions & 0 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,177 @@ This command requires at least Git 2.45.

* `--from=NAME`: Branch whose upstack commits will be considered.

## Integration

### git-spice integration show {#gs-integration-show}

```
gs integration (int) show
```

Show the configured integration branch

Displays the configured integration branch and the tip branches
that compose it. For each tip, shows whether its hash has drifted
from the hash recorded at the last successful rebuild.

### git-spice integration create {#gs-integration-create}

```
gs integration (int) create (c) <name> [flags]
```

Configure the integration branch

Configures the singleton integration branch for this repo.
The branch is materialized by sequentially merging each
tip onto trunk; it is never given a PR and is invisible to
'gs branch' commands.

Use 'gs integration tip add <branch>' to add tips later,
or pass --tip multiple times here.

**Arguments**

* `name`: Local name of the integration branch

**Flags**

* `--upstream=BRANCH`: Upstream branch name (defaults to local name)
* `--tip=BRANCH,...`: Tip branches to include (repeat to add more)

### git-spice integration delete {#gs-integration-delete}

```
gs integration (int) delete (d,rm)
```

Remove the integration branch configuration

Removes the integration branch configuration. The underlying
Git branch (if any) is not deleted; only the git-spice config
that drives auto-rebuild and submit is removed.

### git-spice integration checkout {#gs-integration-checkout}

```
gs integration (int) checkout (co)
```

Switch to the integration branch

Switches the worktree to the configured integration branch.
Fails if no integration is configured, or if the integration
branch has not yet been materialized (run 'gs integration
rebuild' first).

### git-spice integration rebuild {#gs-integration-rebuild}

```
gs integration (int) rebuild (rb) [flags]
```

Rebuild the integration branch

Regenerates the integration branch by resetting it to trunk
and sequentially merging each configured tip with --no-ff.
Rerere is enabled for the duration of these merges so any
recorded conflict resolutions are replayed automatically.

On conflict, the merge is left in the worktree. Resolve the
conflicting files, commit with 'git merge --continue', then
re-run 'gs integration rebuild' (or 'gs intrb') to resume.

**Flags**

* `--push`: Also push the integration branch after rebuilding

### git-spice integration submit {#gs-integration-submit}

```
gs integration (int) submit (s)
```

Push the integration branch to the remote

Pushes the integration branch to the configured remote with
--force-with-lease against the hash recorded at the previous
successful push.

No change request (PR) is opened: this command only pushes the
branch. Once a manual submit succeeds, 'gs stack submit' and
'gs upstack submit' will keep the published branch in sync with
local rebuilds.

### git-spice integration mark-pushed {#gs-integration-mark-pushed}

```
gs integration (int) mark-pushed [<hash>]
```

Record a hash as the integration branch's last-pushed value

Records the given commit hash as the integration branch's
last-pushed value in gs's local state. Does not push.

Used to reconcile state after a manual git push of the
integration branch, or after a "push rejected" error caused by
a multi-checkout collision or a state reset.

With no argument, the hash is discovered from the configured
push remote. With an explicit hash, that hash is recorded
verbatim.

After 'gs integration mark-pushed', the next
'gs integration submit' uses --force-with-lease against the
recorded hash. If multiple checkouts are publishing this
branch, this command will not save you from a collision; it
just confirms which remote state you accept as your baseline
before overwriting.

**Arguments**

* `hash`: Commit hash to record as last-pushed. If empty, fetches the configured push remote and uses its current tip.

### git-spice integration tip add {#gs-integration-tip-add}

```
gs integration (int) tip add (a) <branches> ...
```

Add a branch to the integration tip list

Adds one or more tracked branches to the integration tip list.
Each branch must already be tracked by git-spice; this command
does not track new branches.

Branches are added in order. If one fails to add, the previous
ones remain in the tip list.

**Arguments**

* `branches`: Branches to add as tips

### git-spice integration tip remove {#gs-integration-tip-remove}

```
gs integration (int) tip remove (r,rm) <branches> ...
```

Remove a branch from the integration tip list

**Arguments**

* `branches`: Branches to remove from the tip list

### git-spice integration tip list {#gs-integration-tip-list}

```
gs integration (int) tip list (l,ls)
```

List the configured integration tips

## Rebase

### git-spice rebase continue {#gs-rebase-continue}
Expand Down
5 changes: 5 additions & 0 deletions doc/includes/cli-shorthands.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
| gs dsr | [gs downstack restack](/cli/reference.md#gs-downstack-restack) |
| gs dss | [gs downstack submit](/cli/reference.md#gs-downstack-submit) |
| gs dstr | [gs downstack track](/cli/reference.md#gs-downstack-track) |
| gs intc | [gs integration create](/cli/reference.md#gs-integration-create) |
| gs intco | [gs integration checkout](/cli/reference.md#gs-integration-checkout) |
| gs intd | [gs integration delete](/cli/reference.md#gs-integration-delete) |
| gs intrb | [gs integration rebuild](/cli/reference.md#gs-integration-rebuild) |
| gs ints | [gs integration submit](/cli/reference.md#gs-integration-submit) |
| gs ll | [gs log long](/cli/reference.md#gs-log-long) |
| gs ls | [gs log short](/cli/reference.md#gs-log-short) |
| gs rba | [gs rebase abort](/cli/reference.md#gs-rebase-abort) |
Expand Down
1 change: 1 addition & 0 deletions doc/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ nav:
- guide/branch.md
- guide/cr.md
- guide/scripts.md
- guide/integration.md
- guide/limits.md
- guide/troubleshooting.md
- guide/internals.md
Expand Down
110 changes: 110 additions & 0 deletions doc/src/guide/integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
title: Integration branches
icon: octicons/git-merge-16
description: >-
Build a single branch that combines multiple stack tips
for testing or preview deploys.
---

# Integration branches

<!-- gs:version unreleased -->

An **integration branch** is a repo-scoped singleton that is rebuilt by
sequentially merging the tips of multiple tracked branches onto trunk.
It is intended for testing or preview builds that need to combine
several otherwise-independent stacks.

Integration branches are deliberately separate from regular stack
branches:

- They are never given a PR / change request.
- They are invisible to `gs branch` commands.
- They are regenerated, not edited: every rebuild force-overwrites the
branch.

## Configuration

Configure the singleton with $$gs integration create$$:

```freeze language="terminal"
{green}${reset} gs integration create preview --tip feat-a --tip feat-b
```

The branch name (`preview` above) must not equal trunk. Tips must be
tracked branches; an untracked branch is rejected.

Add or remove tips later with $$gs integration tip add$$ and
$$gs integration tip remove$$.

## Rebuilding

Materialize or refresh the branch with $$gs integration rebuild$$:

```freeze language="terminal"
{green}${reset} gs integration rebuild
```

The worktree must be clean. Each tip is merged with `--no-ff`; `rerere`
is enabled for these merges so any recorded conflict resolutions are
replayed automatically. Rebuild can be invoked while the integration
branch itself is checked out; when finished, the worktree remains on
the integration branch.

If a tip conflicts, the merge is left in the worktree for you to
resolve. The conflicting paths are reported. To proceed:

1. Resolve the conflicts.
2. Stage the resolved files with `git add` and commit the merge with
`git merge --continue`.
3. Re-run `gs integration rebuild` (or `gs intrb`). The rebuild
resumes from the next tip after the one you just merged. The
conflict resolution is also recorded by `rerere` so future
rebuilds skip it automatically.

To bail out of the rebuild instead, run `git merge --abort`. The
pending-rebuild state remains; clear it by re-configuring the
integration with `gs integration delete` followed by `gs integration
create`.

## Switching to the integration branch

Use $$gs integration checkout$$ (shorthand: `gs intco`) to switch the
worktree to the integration branch:

```freeze language="terminal"
{green}${reset} gs intco
```

This fails if the branch has not yet been materialized; run
$$gs integration rebuild$$ first.

### Auto-rebuild

After any `gs branch restack`, `gs upstack restack`, `gs stack restack`,
or `gs repo restack`, the integration branch is automatically
regenerated when at least one of its tips has moved. The auto-rebuild
is silent when no tip has drifted.

## Submitting

Push the branch to the remote with $$gs integration submit$$:

```freeze language="terminal"
{green}${reset} gs integration submit
```

This pushes with `--force-with-lease` against the hash recorded at the
last successful push. **No change request is created.**

### Auto-submit

Once the integration branch has been pushed manually at least once,
subsequent `gs stack submit`, `gs upstack submit`, and
`gs downstack submit` invocations will keep the published branch in
sync by pushing the latest rebuild.

## Removing the configuration

Use $$gs integration delete$$ to remove the configuration. The
underlying Git branch is not deleted.
36 changes: 36 additions & 0 deletions integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"

"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/handler/integration"
)

type integrationCmd struct {
Show integrationShowCmd `cmd:"" help:"Show the configured integration branch" default:"withargs"`
Create integrationCreateCmd `cmd:"" aliases:"c" help:"Configure the integration branch"`
Delete integrationDeleteCmd `cmd:"" aliases:"d,rm" help:"Remove the integration branch configuration"`
Checkout integrationCheckoutCmd `cmd:"" aliases:"co" help:"Switch to the integration branch"`
Rebuild integrationRebuildCmd `cmd:"" aliases:"rb" help:"Rebuild the integration branch"`
Submit integrationSubmitCmd `cmd:"" aliases:"s" help:"Push the integration branch to the remote"`
MarkPushed integrationMarkPushedCmd `cmd:"" name:"mark-pushed" help:"Record a hash as the integration branch's last-pushed value"`
Tip integrationTipCmd `cmd:"" help:"Manage the tips composing the integration branch"`
}

// IntegrationHandler implements integration branch operations.
type IntegrationHandler interface {
Create(ctx context.Context, req *integration.CreateRequest) error
Delete(ctx context.Context) error
AddTip(ctx context.Context, branch string) error
RemoveTip(ctx context.Context, branch string) error
Show(ctx context.Context) (*integration.Status, error)
Checkout(ctx context.Context) error
Rebuild(ctx context.Context) (*integration.RebuildResult, error)
Submit(ctx context.Context) error
MarkPushed(ctx context.Context, hash git.Hash) error
MaybeRebuild(ctx context.Context) error
MaybeRebuildAndSubmit(ctx context.Context) error
}

var _ IntegrationHandler = (*integration.Handler)(nil)
31 changes: 31 additions & 0 deletions integration_checkout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"context"

"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/text"
)

type integrationCheckoutCmd struct{}

func (*integrationCheckoutCmd) Help() string {
return text.Dedent(`
Switches the worktree to the configured integration branch.
Fails if no integration is configured, or if the integration
branch has not yet been materialized (run 'gs integration
rebuild' first).
`)
}

func (cmd *integrationCheckoutCmd) Run(
ctx context.Context,
log *silog.Logger,
handler IntegrationHandler,
) error {
if err := handler.Checkout(ctx); err != nil {
return err
}
log.Info("Switched to integration branch.")
return nil
}
Loading
Loading