mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
36b278467b
Expose --atomic for multi-ref pushes through the shared Git push infrastructure. Keep push non-atomic by default and preserve sync's existing atomic default with --atomic=false available as an opt-out. Document the command behavior, audit link and submit push paths, and add unit and integration coverage for atomic rejection semantics.
145 lines
4.2 KiB
Go
145 lines
4.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/github/gh-stack/internal/config"
|
|
"github.com/github/gh-stack/internal/git"
|
|
"github.com/github/gh-stack/internal/modify"
|
|
"github.com/github/gh-stack/internal/stack"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type pushOptions struct {
|
|
remote string
|
|
atomic bool
|
|
}
|
|
|
|
func PushCmd(cfg *config.Config) *cobra.Command {
|
|
opts := &pushOptions{}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "push",
|
|
Short: "Push active branches in the current stack to the remote",
|
|
Long: `Push active branches in the current stack to the remote.
|
|
|
|
Uses explicit per-branch --force-with-lease checks. By default, updates are not
|
|
atomic: a branch may update even if another branch is rejected. Use --atomic
|
|
to require all branch updates to succeed or fail together.
|
|
Merged and queued branches are automatically skipped.`,
|
|
Example: ` # Push active stack branches to the default remote
|
|
$ gh stack push
|
|
|
|
# Push all active branches atomically
|
|
$ gh stack push --atomic
|
|
|
|
# Push to a specific remote
|
|
$ gh stack push --remote upstream`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runPush(cfg, opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&opts.remote, "remote", "", "Remote to push to (defaults to auto-detected remote)")
|
|
cmd.Flags().BoolVar(&opts.atomic, "atomic", false, "Require all branch updates to succeed or fail together (default: disabled)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runPush(cfg *config.Config, opts *pushOptions) error {
|
|
gitDir, err := git.GitDir()
|
|
if err != nil {
|
|
cfg.Errorf("not a git repository")
|
|
return ErrNotInStack
|
|
}
|
|
|
|
if err := modify.CheckStateGuard(gitDir); err != nil {
|
|
cfg.Errorf("%s", err)
|
|
return ErrModifyRecovery
|
|
}
|
|
|
|
sf, err := stack.Load(gitDir)
|
|
if err != nil {
|
|
cfg.Errorf("failed to load stack state: %s", err)
|
|
return ErrNotInStack
|
|
}
|
|
|
|
currentBranch, err := git.CurrentBranch()
|
|
if err != nil {
|
|
cfg.Errorf("failed to get current branch: %s", err)
|
|
return ErrNotInStack
|
|
}
|
|
|
|
// Find the stack for the current branch without switching branches.
|
|
// Push should never change the user's checked-out branch.
|
|
stacks := sf.FindAllStacksForBranch(currentBranch)
|
|
if len(stacks) == 0 {
|
|
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
|
|
return ErrNotInStack
|
|
}
|
|
if len(stacks) > 1 {
|
|
cfg.Errorf("branch %q belongs to multiple stacks; checkout a non-trunk branch first", currentBranch)
|
|
return ErrDisambiguate
|
|
}
|
|
s := stacks[0]
|
|
|
|
// Push all active branches with explicit per-branch leases.
|
|
remote, err := pickRemote(cfg, currentBranch, opts.remote)
|
|
if err != nil {
|
|
if !errors.Is(err, errInterrupt) {
|
|
cfg.Errorf("%s", err)
|
|
}
|
|
return ErrSilent
|
|
}
|
|
// Sync PR state to detect merged/queued PRs before pushing.
|
|
_ = syncStackPRs(cfg, s)
|
|
|
|
merged := s.MergedBranches()
|
|
if len(merged) > 0 {
|
|
cfg.Printf("Skipping %d merged %s", len(merged), plural(len(merged), "branch", "branches"))
|
|
}
|
|
queued := s.QueuedBranches()
|
|
if len(queued) > 0 {
|
|
cfg.Printf("Skipping %d queued %s", len(queued), plural(len(queued), "branch", "branches"))
|
|
}
|
|
activeBranches := activeBranchNames(s)
|
|
if len(activeBranches) == 0 {
|
|
cfg.Printf("No active branches to push (all merged or queued)")
|
|
return nil
|
|
}
|
|
// Best-effort fetch to update tracking refs (helps --force-with-lease
|
|
// in shallow clones). Silently ignored if branches don't exist on the
|
|
// remote yet.
|
|
_ = git.FetchBranches(remote, activeBranches)
|
|
cfg.Printf("Pushing %d %s to %s...", len(activeBranches), plural(len(activeBranches), "branch", "branches"), remote)
|
|
if err := git.Push(remote, activeBranches, true, opts.atomic); err != nil {
|
|
cfg.Errorf("failed to push: %s", err)
|
|
return ErrSilent
|
|
}
|
|
|
|
// Update base commit hashes after push
|
|
updateBaseSHAs(s)
|
|
|
|
if err := stack.Save(gitDir, sf); err != nil {
|
|
return handleSaveError(cfg, err)
|
|
}
|
|
|
|
cfg.Successf("Pushed %d branches", len(activeBranches))
|
|
|
|
// Hint about submit only if there are branches without PRs
|
|
hasBranchWithoutPR := false
|
|
for _, b := range s.ActiveBranches() {
|
|
if b.PullRequest == nil {
|
|
hasBranchWithoutPR = true
|
|
break
|
|
}
|
|
}
|
|
if hasBranchWithoutPR {
|
|
cfg.Printf("To create PRs for this stack, run `%s`",
|
|
cfg.ColorCyan("gh stack submit"))
|
|
} else {
|
|
cfg.Printf("Run `%s` to see your stack of PRs", cfg.ColorCyan("gh stack view"))
|
|
}
|
|
return nil
|
|
}
|