-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathcommit_split.go
114 lines (98 loc) · 2.92 KB
/
commit_split.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
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 commitSplitCmd struct {
Message string `short:"m" placeholder:"MSG" help:"Use the given message as the commit message."`
NoVerify bool `help:"Bypass pre-commit and commit-msg hooks."`
}
func (*commitSplitCmd) Help() string {
return text.Dedent(`
Interactively select hunks from the current commit
to split into new commits below it.
Branches upstack are restacked as needed.
`)
}
func (cmd *commitSplitCmd) Run(
ctx context.Context,
log *log.Logger,
repo *git.Repository,
store *state.Store,
svc *spice.Service,
) (err error) {
head, err := repo.Head(ctx)
if err != nil {
return fmt.Errorf("get HEAD: %w", err)
}
parent, err := repo.PeelToCommit(ctx, head.String()+"^")
if err != nil {
return fmt.Errorf("get HEAD^: %w", err)
}
if err := repo.Reset(ctx, parent.String(), git.ResetOptions{
Mode: git.ResetMixed, // don't touch the working tree
}); err != nil {
return fmt.Errorf("reset to HEAD^: %w", err)
}
defer func() {
if err != nil {
// The operation may have failed
// because the user pressed Ctrl-C.
// That would invalidate the current context.
// Create an uncanceled context to perform the rollback.
ctx := context.WithoutCancel(ctx)
log.Warn("rolling back to previous commit", "commit", head)
err = errors.Join(err, repo.Reset(ctx, head.String(), git.ResetOptions{
Mode: git.ResetMixed,
}))
}
}()
log.Info("Select hunks to extract into a new commit")
// Can't use 'git add' here because reset will have unstaged
// new files, which 'git add' will ignore.
if err := repo.Reset(ctx, head.String(), git.ResetOptions{Patch: true}); err != nil {
return fmt.Errorf("select hunks: %w", err)
}
if err := repo.Commit(ctx, git.CommitRequest{
Message: cmd.Message,
NoVerify: cmd.NoVerify,
}); err != nil {
return fmt.Errorf("commit: %w", err)
}
if err := repo.Reset(ctx, head.String(), git.ResetOptions{
Paths: []string{"."}, // reset index to remaining changes
}); err != nil {
return fmt.Errorf("select hunks: %w", err)
}
// Commit will move HEAD to the new commit,
// updating branch ref if necessary.
if err := repo.Commit(ctx, git.CommitRequest{
ReuseMessage: head.String(),
NoVerify: cmd.NoVerify,
}); err != nil {
return fmt.Errorf("commit: %w", err)
}
if _, err := repo.RebaseState(ctx); err == nil {
// In the middle of a rebase.
// Don't restack upstack branches.
return nil
}
currentBranch, err := repo.CurrentBranch(ctx)
if err != nil {
// No restack needed if we're in a detached head state.
if errors.Is(err, git.ErrDetachedHead) {
return nil
}
return fmt.Errorf("get current branch: %w", err)
}
return (&upstackRestackCmd{
Branch: currentBranch,
SkipStart: true,
}).Run(ctx, log, repo, store, svc)
}