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/Fixed-20260601-073704.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Fixed
body: 'repo sync: detect locally-merged branches on supported forges when commits land on trunk via direct push, cleaning up the stack without restack conflicts.'
time: 2026-06-01T07:37:04.600167-04:00
2 changes: 1 addition & 1 deletion branch_restack.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ func (cmd *branchRestackCmd) AfterApply(ctx context.Context, wt *git.Worktree) e
}

func (cmd *branchRestackCmd) Run(ctx context.Context, handler RestackHandler) error {
return handler.RestackBranch(ctx, cmd.Branch)
return handler.RestackBranch(ctx, cmd.Branch, nil)
}
2 changes: 1 addition & 1 deletion downstack_restack.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ func (cmd *downstackRestackCmd) Run(
return errors.New("nothing to restack below trunk")
}

return handler.RestackDownstack(ctx, cmd.Branch)
return handler.RestackDownstack(ctx, cmd.Branch, nil)
}
5 changes: 3 additions & 2 deletions internal/handler/merge/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"go.abhg.dev/gs/internal/handler/restack"
"go.abhg.dev/gs/internal/handler/submit"
"go.abhg.dev/gs/internal/handler/sync"

Expand All @@ -36,7 +37,7 @@ type Service interface {

// RestackHandler restacks branches after their bases are merged.
type RestackHandler interface {
RestackBranch(context.Context, string) error
RestackBranch(ctx context.Context, branch string, opts *restack.Options) error
}

// SubmitHandler updates change requests after branch restacks.
Expand Down Expand Up @@ -599,7 +600,7 @@ func (e *mergePlanExecutor) prepareForMerge(
return fmt.Errorf("verify restacked: %w", err)
}

if err := e.Restack.RestackBranch(ctx, item.branch); err != nil {
if err := e.Restack.RestackBranch(ctx, item.branch, nil); err != nil {
return fmt.Errorf("restack branch: %w", err)
}

Expand Down
6 changes: 3 additions & 3 deletions internal/handler/merge/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,10 @@ func TestExecutePlan_retargets(t *testing.T) {

mockRestack := NewMockRestackHandler(ctrl)
mockRestack.EXPECT().
RestackBranch(gomock.Any(), "feat2").
RestackBranch(gomock.Any(), "feat2", gomock.Nil()).
Return(nil)
mockRestack.EXPECT().
RestackBranch(gomock.Any(), "feat3").
RestackBranch(gomock.Any(), "feat3", gomock.Nil()).
Return(nil)

mockSubmit := NewMockSubmitHandler(ctrl)
Expand Down Expand Up @@ -396,7 +396,7 @@ func TestExecutePlan_waitsForPreparedChangeHeadBeforeChecks(t *testing.T) {

mockRestack := NewMockRestackHandler(ctrl)
mockRestack.EXPECT().
RestackBranch(gomock.Any(), "feat2").
RestackBranch(gomock.Any(), "feat2", gomock.Nil()).
Return(nil)

mockSubmit := NewMockSubmitHandler(ctrl)
Expand Down
13 changes: 7 additions & 6 deletions internal/handler/merge/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion internal/handler/restack/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package restack
import "context"

// RestackBranch restacks the given branch onto its base.
func (h *Handler) RestackBranch(ctx context.Context, branch string) error {
func (h *Handler) RestackBranch(
ctx context.Context, branch string, opts *Options,
) error {
_, err := h.Restack(ctx, &Request{
Branch: branch,
ContinueCommand: []string{"branch", "restack"},
AutoResolve: opts.autoResolvePtr(),
})
return err
}
10 changes: 5 additions & 5 deletions internal/handler/restack/branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestHandler_RestackBranch(t *testing.T) {
Store: statetest.NewMemoryStore(t, "main", "", log),
Service: mockService,
}
require.NoError(t, handler.RestackBranch(t.Context(), "feature"))
require.NoError(t, handler.RestackBranch(t.Context(), "feature", nil))
assert.Contains(t, logBuffer.String(), "feature: restacked on main")
})

Expand Down Expand Up @@ -82,7 +82,7 @@ func TestHandler_RestackBranch(t *testing.T) {
Service: mockService,
}

require.NoError(t, handler.RestackBranch(t.Context(), "feature"))
require.NoError(t, handler.RestackBranch(t.Context(), "feature", nil))
})

t.Run("UntrackedBranch", func(t *testing.T) {
Expand Down Expand Up @@ -113,7 +113,7 @@ func TestHandler_RestackBranch(t *testing.T) {
Service: mockService,
}

err := handler.RestackBranch(t.Context(), "untracked")
err := handler.RestackBranch(t.Context(), "untracked", nil)
require.Error(t, err)
assert.ErrorContains(t, err, "untracked branch")
assert.Contains(t, logBuffer.String(), "untracked: branch not tracked: run '"+cli.Name()+" branch track")
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestHandler_RestackBranch(t *testing.T) {
Store: statetest.NewMemoryStore(t, "main", "", log),
Service: mockService,
}
require.NoError(t, handler.RestackBranch(t.Context(), "already-restacked"))
require.NoError(t, handler.RestackBranch(t.Context(), "already-restacked", nil))
assert.Contains(t, logBuffer.String(), "already-restacked: branch does not need to be restacked.")
})

Expand Down Expand Up @@ -177,7 +177,7 @@ func TestHandler_RestackBranch(t *testing.T) {
Store: statetest.NewMemoryStore(t, "main", "", log),
Service: mockService,
}
err := handler.RestackBranch(t.Context(), "feature")
err := handler.RestackBranch(t.Context(), "feature", nil)
require.Error(t, err)
assert.ErrorContains(t, err, "restack branch")
assert.ErrorIs(t, err, unexpectedErr)
Expand Down
5 changes: 4 additions & 1 deletion internal/handler/restack/downstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import "context"

// RestackDownstack restacks the downstack of the given branch.
// This includes the branch itself.
func (h *Handler) RestackDownstack(ctx context.Context, branch string) error {
func (h *Handler) RestackDownstack(
ctx context.Context, branch string, opts *Options,
) error {
_, err := h.Restack(ctx, &Request{
Branch: branch,
Scope: ScopeDownstack,
ContinueCommand: []string{"downstack", "restack"},
AutoResolve: opts.autoResolvePtr(),
})
return err
}
4 changes: 2 additions & 2 deletions internal/handler/restack/downstack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestHandler_RestackDownstack(t *testing.T) {
Service: mockService,
}

require.NoError(t, handler.RestackDownstack(t.Context(), "feature3"))
require.NoError(t, handler.RestackDownstack(t.Context(), "feature3", nil))
assert.Contains(t, logBuffer.String(), "feature1: restacked on main")
assert.Contains(t, logBuffer.String(), "feature2: restacked on feature1")
assert.Contains(t, logBuffer.String(), "feature3: restacked on feature2")
Expand Down Expand Up @@ -99,6 +99,6 @@ func TestHandler_RestackDownstack(t *testing.T) {
Service: mockService,
}

require.NoError(t, handler.RestackDownstack(t.Context(), "feature2"))
require.NoError(t, handler.RestackDownstack(t.Context(), "feature2", nil))
})
}
27 changes: 27 additions & 0 deletions internal/handler/restack/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ type Request struct {
//
// Defaults to ScopeBranch.
Scope Scope

// AutoResolve, if non-nil, overrides
// [Handler.DefaultAutoResolve] for this invocation. A true
// value enables the configured resolver; a false value disables
// it even when configured.
AutoResolve *bool
}

// Options are optional parameters shared by the high-level restack
// entry points ([Handler.RestackBranch], [Handler.RestackStack],
// [Handler.RestackDownstack]).
type Options struct {
// AutoResolve, if non-nil, overrides
// [Handler.DefaultAutoResolve] for this invocation. A true
// value enables the configured resolver; a false value disables
// it even when configured.
AutoResolve *bool
}

// autoResolvePtr returns the AutoResolve pointer if opts is
// non-nil, or nil. Callers pass the result through to
// [Request.AutoResolve].
func (o *Options) autoResolvePtr() *bool {
if o == nil {
return nil
}
return o.AutoResolve
}

// Restack restacks one or more branches according to the request.
Expand Down
5 changes: 4 additions & 1 deletion internal/handler/restack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import "context"
// RestackStack restacks the stack of the given branch.
// This includes all upstack and downtrack branches,
// as well as the branch itself.
func (h *Handler) RestackStack(ctx context.Context, branch string) error {
func (h *Handler) RestackStack(
ctx context.Context, branch string, opts *Options,
) error {
_, err := h.Restack(ctx, &Request{
Branch: branch,
Scope: ScopeStack,
ContinueCommand: []string{"stack", "restack"},
AutoResolve: opts.autoResolvePtr(),
})
return err
}
Loading
Loading