-
Notifications
You must be signed in to change notification settings - Fork 17
/
branch_fold.go
117 lines (99 loc) · 3.19 KB
/
branch_fold.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package main
import (
"context"
"errors"
"fmt"
"github.com/charmbracelet/log"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
"go.abhg.dev/gs/internal/text"
)
type branchFoldCmd struct {
Branch string `placeholder:"NAME" help:"Name of the branch" predictor:"trackedBranches"`
}
func (*branchFoldCmd) Help() string {
return text.Dedent(`
Commits from the current branch will be merged into its base
and the current branch will be deleted.
Branches above the folded branch will point
to the next branch downstack.
Use the --branch flag to target a different branch.
`)
}
func (cmd *branchFoldCmd) Run(ctx context.Context, log *log.Logger, opts *globalOptions) error {
repo, store, svc, err := openRepo(ctx, log, opts)
if err != nil {
return err
}
if cmd.Branch == "" {
currentBranch, err := repo.CurrentBranch(ctx)
if err != nil {
return fmt.Errorf("get current branch: %w", err)
}
cmd.Branch = currentBranch
}
if err := svc.VerifyRestacked(ctx, cmd.Branch); err != nil {
var restackErr *spice.BranchNeedsRestackError
switch {
case errors.Is(err, state.ErrNotExist):
return fmt.Errorf("branch %v not tracked", cmd.Branch)
case errors.As(err, &restackErr):
return fmt.Errorf("branch %v needs to be restacked before it can be folded", cmd.Branch)
default:
return fmt.Errorf("verify restacked: %w", err)
}
}
b, err := svc.LookupBranch(ctx, cmd.Branch)
if err != nil {
return fmt.Errorf("get branch: %w", err)
}
// Merge base into current branch using a fast-forward.
// To do this without checking out the base, we can use a local fetch
// and fetch the feature branch "into" the base branch.
if err := repo.Fetch(ctx, git.FetchOptions{
Remote: ".", // local repository
Refspecs: []git.Refspec{
git.Refspec(cmd.Branch + ":" + b.Base),
},
}); err != nil {
return fmt.Errorf("update base branch: %w", err)
}
newBaseHash, err := repo.PeelToCommit(ctx, b.Base)
if err != nil {
return fmt.Errorf("peel to commit: %w", err)
}
tx := store.BeginBranchTx()
// Change the base of all branches above us
// to the base of the branch we are folding.
aboves, err := svc.ListAbove(ctx, cmd.Branch)
if err != nil {
return fmt.Errorf("list above: %w", err)
}
for _, above := range aboves {
if err := tx.Upsert(ctx, state.UpsertRequest{
Name: above,
Base: b.Base,
BaseHash: newBaseHash,
}); err != nil {
return fmt.Errorf("set base of %v to %v: %w", above, b.Base, err)
}
}
if err := tx.Delete(ctx, cmd.Branch); err != nil {
return fmt.Errorf("delete branch %v from state: %w", cmd.Branch, err)
}
if err := tx.Commit(ctx, fmt.Sprintf("folding %v into %v", cmd.Branch, b.Base)); err != nil {
return fmt.Errorf("update state: %w", err)
}
// Check out base and delete the branch we are folding.
if err := (&branchCheckoutCmd{Branch: b.Base}).Run(ctx, log, opts); err != nil {
return fmt.Errorf("checkout base: %w", err)
}
if err := repo.DeleteBranch(ctx, cmd.Branch, git.BranchDeleteOptions{
Force: true, // we know it's merged
}); err != nil {
return fmt.Errorf("delete branch: %w", err)
}
log.Infof("Branch %v has been folded into %v", cmd.Branch, b.Base)
return nil
}