mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
fded736e78
* clarify push behavior * max stack size * clarify only pushing active branches * no stack obj in pull_request.opened event
140 lines
4.0 KiB
Go
140 lines
4.0 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
|
|
}
|
|
|
|
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. Updates are not atomic: a
|
|
branch may update even if another branch is rejected. Fix the rejected branch
|
|
and run the command again; branches already updated will be unchanged.
|
|
Merged and queued branches are automatically skipped.`,
|
|
Example: ` # Push active stack branches to the default remote
|
|
$ gh stack push
|
|
|
|
# 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)")
|
|
|
|
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, false); 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
|
|
}
|