|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + |
| 7 | + "github.com/cli/go-gh/v2/pkg/prompter" |
| 8 | + "github.com/github/gh-stack/internal/config" |
| 9 | + "github.com/github/gh-stack/internal/git" |
| 10 | + "github.com/github/gh-stack/internal/stack" |
| 11 | + "github.com/spf13/cobra" |
| 12 | +) |
| 13 | + |
| 14 | +type initOptions struct { |
| 15 | + branches []string |
| 16 | + base string |
| 17 | + adopt bool |
| 18 | +} |
| 19 | + |
| 20 | +func InitCmd(cfg *config.Config) *cobra.Command { |
| 21 | + opts := &initOptions{} |
| 22 | + |
| 23 | + cmd := &cobra.Command{ |
| 24 | + Use: "init [branches...]", |
| 25 | + Short: "Initialize a new stack", |
| 26 | + Long: `Initialize a stack object in the local repo. |
| 27 | +
|
| 28 | +Creates an entry in .git/gh-stack to track stack state. |
| 29 | +Unless specified, prompts user to create/select branch for first layer of the stack. |
| 30 | +Trunk defaults to default branch, unless specified otherwise.`, |
| 31 | + Example: ` $ gh stack init |
| 32 | + $ gh stack init myBranch |
| 33 | + $ gh stack init branch1 branch2 branch3 --adopt |
| 34 | + $ gh stack init firstBranch -b integrationBranch`, |
| 35 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 36 | + opts.branches = args |
| 37 | + return runInit(cfg, opts) |
| 38 | + }, |
| 39 | + } |
| 40 | + |
| 41 | + cmd.Flags().StringVarP(&opts.base, "base", "b", "", "Trunk branch for stack (defaults to default branch)") |
| 42 | + cmd.Flags().BoolVarP(&opts.adopt, "adopt", "a", false, "Track existing branches as part of a stack") |
| 43 | + |
| 44 | + return cmd |
| 45 | +} |
| 46 | + |
| 47 | +func runInit(cfg *config.Config, opts *initOptions) error { |
| 48 | + gitDir, err := git.GitDir() |
| 49 | + if err != nil { |
| 50 | + cfg.Errorf("not a git repository") |
| 51 | + return nil |
| 52 | + } |
| 53 | + |
| 54 | + // Determine trunk branch |
| 55 | + trunk := opts.base |
| 56 | + if trunk == "" { |
| 57 | + trunk, err = git.DefaultBranch() |
| 58 | + if err != nil { |
| 59 | + cfg.Errorf("unable to determine default branch: %s\nUse -b to specify the trunk branch", err) |
| 60 | + return nil |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // Load existing stack file |
| 65 | + sf, err := stack.Load(gitDir) |
| 66 | + if err != nil { |
| 67 | + cfg.Errorf("failed to load stack state: %s", err) |
| 68 | + return nil |
| 69 | + } |
| 70 | + |
| 71 | + // Set repository context |
| 72 | + repo, err := cfg.Repo() |
| 73 | + if err == nil { |
| 74 | + sf.Repository = repo.Owner + "/" + repo.Name |
| 75 | + } |
| 76 | + |
| 77 | + currentBranch, _ := git.CurrentBranch() |
| 78 | + |
| 79 | + var branches []string |
| 80 | + |
| 81 | + if opts.adopt { |
| 82 | + // Adopt mode: validate all specified branches exist |
| 83 | + if len(opts.branches) == 0 { |
| 84 | + cfg.Errorf("--adopt requires at least one branch name") |
| 85 | + return nil |
| 86 | + } |
| 87 | + for _, b := range opts.branches { |
| 88 | + if !git.BranchExists(b) { |
| 89 | + cfg.Errorf("branch %q does not exist", b) |
| 90 | + return nil |
| 91 | + } |
| 92 | + if err := sf.ValidateNoDuplicateBranch(b); err != nil { |
| 93 | + cfg.Errorf("branch %q already exists in the stack", b) |
| 94 | + return nil |
| 95 | + } |
| 96 | + } |
| 97 | + branches = opts.branches |
| 98 | + } else if len(opts.branches) > 0 { |
| 99 | + // Explicit branch names provided — create them |
| 100 | + for _, b := range opts.branches { |
| 101 | + if err := sf.ValidateNoDuplicateBranch(b); err != nil { |
| 102 | + cfg.Errorf("branch %q already exists in the stack", b) |
| 103 | + return nil |
| 104 | + } |
| 105 | + if !git.BranchExists(b) { |
| 106 | + if err := git.CreateBranch(b, trunk); err != nil { |
| 107 | + cfg.Errorf("creating branch %s: %s", b, err) |
| 108 | + return nil |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + branches = opts.branches |
| 113 | + } else { |
| 114 | + // Interactive mode |
| 115 | + p := prompter.New(os.Stdin, os.Stdout, os.Stderr) |
| 116 | + |
| 117 | + if currentBranch != "" && currentBranch != trunk { |
| 118 | + // Already on a non-trunk branch — offer to use it |
| 119 | + useCurrentBranch, err := p.Confirm( |
| 120 | + fmt.Sprintf("Would you like to use %s as the first layer of your stack?", currentBranch), |
| 121 | + true, |
| 122 | + ) |
| 123 | + if err != nil { |
| 124 | + cfg.Errorf("failed to confirm branch selection: %s", err) |
| 125 | + return nil |
| 126 | + } |
| 127 | + if useCurrentBranch { |
| 128 | + if err := sf.ValidateNoDuplicateBranch(currentBranch); err != nil { |
| 129 | + cfg.Errorf("branch %q already exists in the stack", currentBranch) |
| 130 | + return nil |
| 131 | + } |
| 132 | + branches = []string{currentBranch} |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + if len(branches) == 0 { |
| 137 | + branchName, err := p.Input("What branch would you like to use as the first layer of your stack?", "") |
| 138 | + if err != nil { |
| 139 | + cfg.Errorf("failed to read branch name: %s", err) |
| 140 | + return nil |
| 141 | + } |
| 142 | + if branchName == "" { |
| 143 | + cfg.Errorf("branch name cannot be empty") |
| 144 | + return nil |
| 145 | + } |
| 146 | + if err := sf.ValidateNoDuplicateBranch(branchName); err != nil { |
| 147 | + cfg.Errorf("branch %q already exists in the stack", branchName) |
| 148 | + return nil |
| 149 | + } |
| 150 | + if !git.BranchExists(branchName) { |
| 151 | + if err := git.CreateBranch(branchName, trunk); err != nil { |
| 152 | + cfg.Errorf("creating branch %s: %s", branchName, err) |
| 153 | + return nil |
| 154 | + } |
| 155 | + } |
| 156 | + branches = []string{branchName} |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // Build stack |
| 161 | + trunkSHA, _ := git.HeadSHA(trunk) |
| 162 | + branchRefs := make([]stack.BranchRef, len(branches)) |
| 163 | + for i, b := range branches { |
| 164 | + sha, _ := git.HeadSHA(b) |
| 165 | + branchRefs[i] = stack.BranchRef{Branch: b, Head: sha} |
| 166 | + } |
| 167 | + |
| 168 | + newStack := stack.Stack{ |
| 169 | + Trunk: stack.BranchRef{ |
| 170 | + Branch: trunk, |
| 171 | + Head: trunkSHA, |
| 172 | + }, |
| 173 | + Branches: branchRefs, |
| 174 | + } |
| 175 | + |
| 176 | + sf.AddStack(newStack) |
| 177 | + if err := stack.Save(gitDir, sf); err != nil { |
| 178 | + return err |
| 179 | + } |
| 180 | + |
| 181 | + // Print result |
| 182 | + if opts.adopt { |
| 183 | + cfg.Printf("Adopting stack with trunk %s and %d branches", trunk, len(branches)) |
| 184 | + chainParts := []string{"(" + trunk + ")"} |
| 185 | + for _, b := range branches { |
| 186 | + chainParts = append(chainParts, b) |
| 187 | + } |
| 188 | + cfg.Printf("Initializing stack: %s", joinChain(chainParts)) |
| 189 | + cfg.Printf("You can continue working on %s", branches[len(branches)-1]) |
| 190 | + } else { |
| 191 | + cfg.Successf("Creating stack with trunk %s and branch %s", trunk, branches[len(branches)-1]) |
| 192 | + // Switch to last branch if not already there |
| 193 | + lastBranch := branches[len(branches)-1] |
| 194 | + if currentBranch != lastBranch { |
| 195 | + if err := git.CheckoutBranch(lastBranch); err != nil { |
| 196 | + cfg.Errorf("switching to branch %s: %s", lastBranch, err) |
| 197 | + return nil |
| 198 | + } |
| 199 | + cfg.Printf("Switched to branch %s", lastBranch) |
| 200 | + } else { |
| 201 | + cfg.Printf("You can continue working on %s", lastBranch) |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + cfg.Printf("To add a new layer to your stack, run %s", cfg.ColorCyan("gh stack add")) |
| 206 | + cfg.Printf("When you're ready to push to GitHub and open a stack of PRs, run %s", cfg.ColorCyan("gh stack push")) |
| 207 | + |
| 208 | + return nil |
| 209 | +} |
| 210 | + |
| 211 | +// joinChain formats branches as: (trunk) <- branch1 <- branch2 |
| 212 | +func joinChain(parts []string) string { |
| 213 | + result := parts[0] |
| 214 | + for _, p := range parts[1:] { |
| 215 | + result += " <- " + p |
| 216 | + } |
| 217 | + return result |
| 218 | +} |
0 commit comments