Skip to content

Commit 0dab37a

Browse files
authored
Allow reverting a range of commits (#4444)
- **PR Description** This is part four of a four part series of PRs that improve the cherry-pick and revert experience. With this PR we support reverting a range selection of commits. Fixes #3272
2 parents c13c635 + 945b023 commit 0dab37a

File tree

7 files changed

+81
-118
lines changed

7 files changed

+81
-118
lines changed

Diff for: pkg/commands/git_commands/commit.go

+8-9
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,14 @@ func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string)
285285
return self.cmd.New(cmdArgs).DontLog()
286286
}
287287

288-
// Revert reverts the selected commit by hash
289-
func (self *CommitCommands) Revert(hash string) error {
290-
cmdArgs := NewGitCmd("revert").Arg(hash).ToArgv()
291-
292-
return self.cmd.New(cmdArgs).Run()
293-
}
294-
295-
func (self *CommitCommands) RevertMerge(hash string, parentNumber int) error {
296-
cmdArgs := NewGitCmd("revert").Arg(hash, "-m", fmt.Sprintf("%d", parentNumber)).
288+
// Revert reverts the selected commits by hash. If isMerge is true, we'll pass -m 1
289+
// to say we want to revert the first parent of the merge commit, which is the one
290+
// people want in 99.9% of cases. In current git versions we could unconditionally
291+
// pass -m 1 even for non-merge commits, but older versions of git choke on it.
292+
func (self *CommitCommands) Revert(hashes []string, isMerge bool) error {
293+
cmdArgs := NewGitCmd("revert").
294+
ArgIf(isMerge, "-m", "1").
295+
Arg(hashes...).
297296
ToArgv()
298297

299298
return self.cmd.New(cmdArgs).Run()

Diff for: pkg/gui/controllers/local_commits_controller.go

+21-47
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package controllers
22

33
import (
4-
"fmt"
54
"strings"
65

76
"github.com/go-errors/errors"
@@ -249,8 +248,8 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
249248
},
250249
{
251250
Key: opts.GetKey(opts.Config.Commits.RevertCommit),
252-
Handler: self.withItem(self.revert),
253-
GetDisabledReason: self.require(self.singleItemSelected()),
251+
Handler: self.withItemsRange(self.revert),
252+
GetDisabledReason: self.require(self.itemRangeSelected()),
254253
Description: self.c.Tr.Revert,
255254
Tooltip: self.c.Tr.RevertCommitTooltip,
256255
},
@@ -858,66 +857,41 @@ func (self *LocalCommitsController) addCoAuthor(start, end int) error {
858857
return nil
859858
}
860859

861-
func (self *LocalCommitsController) revert(commit *models.Commit) error {
862-
if commit.IsMerge() {
863-
return self.createRevertMergeCommitMenu(commit)
860+
func (self *LocalCommitsController) revert(commits []*models.Commit, start, end int) error {
861+
var promptText string
862+
if len(commits) == 1 {
863+
promptText = utils.ResolvePlaceholderString(
864+
self.c.Tr.ConfirmRevertCommit,
865+
map[string]string{
866+
"selectedCommit": commits[0].ShortHash(),
867+
})
868+
} else {
869+
promptText = self.c.Tr.ConfirmRevertCommitRange
864870
}
871+
hashes := lo.Map(commits, func(c *models.Commit, _ int) string { return c.Hash })
872+
isMerge := lo.SomeBy(commits, func(c *models.Commit) bool { return c.IsMerge() })
865873

866874
self.c.Confirm(types.ConfirmOpts{
867-
Title: self.c.Tr.Actions.RevertCommit,
868-
Prompt: utils.ResolvePlaceholderString(
869-
self.c.Tr.ConfirmRevertCommit,
870-
map[string]string{
871-
"selectedCommit": commit.ShortHash(),
872-
}),
875+
Title: self.c.Tr.Actions.RevertCommit,
876+
Prompt: promptText,
873877
HandleConfirm: func() error {
874878
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
875879
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
876-
result := self.c.Git().Commit.Revert(commit.Hash)
880+
result := self.c.Git().Commit.Revert(hashes, isMerge)
877881
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(result); err != nil {
878882
return err
879883
}
880-
return self.afterRevertCommit()
884+
self.context().MoveSelection(len(commits))
885+
return self.c.Refresh(types.RefreshOptions{
886+
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
887+
})
881888
})
882889
},
883890
})
884891

885892
return nil
886893
}
887894

888-
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
889-
menuItems := make([]*types.MenuItem, len(commit.Parents))
890-
for i, parentHash := range commit.Parents {
891-
message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentHash)
892-
if err != nil {
893-
return err
894-
}
895-
896-
menuItems[i] = &types.MenuItem{
897-
Label: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentHash, 8), message),
898-
OnPress: func() error {
899-
parentNumber := i + 1
900-
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
901-
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
902-
if err := self.c.Git().Commit.RevertMerge(commit.Hash, parentNumber); err != nil {
903-
return err
904-
}
905-
return self.afterRevertCommit()
906-
})
907-
},
908-
}
909-
}
910-
911-
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.SelectParentCommitForMerge, Items: menuItems})
912-
}
913-
914-
func (self *LocalCommitsController) afterRevertCommit() error {
915-
self.context().MoveSelection(1)
916-
return self.c.Refresh(types.RefreshOptions{
917-
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
918-
})
919-
}
920-
921895
func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) error {
922896
var disabledReasonWhenFilesAreNeeded *types.DisabledReason
923897
if len(self.c.Model().Files) == 0 {

Diff for: pkg/i18n/english.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,6 @@ type TranslationSet struct {
727727
FocusCommandLog string
728728
CommandLogHeader string
729729
RandomTip string
730-
SelectParentCommitForMerge string
731730
ToggleWhitespaceInDiffView string
732731
ToggleWhitespaceInDiffViewTooltip string
733732
IgnoreWhitespaceDiffViewSubTitle string
@@ -772,6 +771,7 @@ type TranslationSet struct {
772771
OpenCommitInBrowser string
773772
ViewBisectOptions string
774773
ConfirmRevertCommit string
774+
ConfirmRevertCommitRange string
775775
RewordInEditorTitle string
776776
RewordInEditorPrompt string
777777
CheckoutAutostashPrompt string
@@ -1795,7 +1795,6 @@ func EnglishTranslationSet() *TranslationSet {
17951795
FocusCommandLog: "Focus command log",
17961796
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
17971797
RandomTip: "Random tip",
1798-
SelectParentCommitForMerge: "Select parent commit for merge",
17991798
ToggleWhitespaceInDiffView: "Toggle whitespace",
18001799
ToggleWhitespaceInDiffViewTooltip: "Toggle whether or not whitespace changes are shown in the diff view.",
18011800
IgnoreWhitespaceDiffViewSubTitle: "(ignoring whitespace)",
@@ -1839,6 +1838,7 @@ func EnglishTranslationSet() *TranslationSet {
18391838
OpenCommitInBrowser: "Open commit in browser",
18401839
ViewBisectOptions: "View bisect options",
18411840
ConfirmRevertCommit: "Are you sure you want to revert {{.selectedCommit}}?",
1841+
ConfirmRevertCommitRange: "Are you sure you want to revert the selected commits?",
18421842
RewordInEditorTitle: "Reword in editor",
18431843
RewordInEditorPrompt: "Are you sure you want to reword this commit in your editor?",
18441844
HardResetAutostashPrompt: "Are you sure you want to hard reset to '%s'? An auto-stash will be performed if necessary.",

Diff for: pkg/integration/tests/commit/revert_merge.go

+3-8
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,9 @@ var RevertMerge = NewIntegrationTest(NewIntegrationTestArgs{
2121
).
2222
Press(keys.Commits.RevertCommit)
2323

24-
t.ExpectPopup().Menu().
25-
Title(Equals("Select parent commit for merge")).
26-
Lines(
27-
Contains("first change"),
28-
Contains("second-change-branch unrelated change"),
29-
Contains("Cancel"),
30-
).
31-
Select(Contains("first change")).
24+
t.ExpectPopup().Confirmation().
25+
Title(Equals("Revert commit")).
26+
Content(MatchesRegexp(`Are you sure you want to revert \w+?`)).
3227
Confirm()
3328

3429
t.Views().Commits().IsFocused().

Diff for: pkg/integration/tests/commit/revert_with_conflict_multiple_commits.go

+13-17
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,7 @@ var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArg
99
Description: "Reverts a range of commits, the first of which conflicts",
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
12-
SetupConfig: func(cfg *config.AppConfig) {
13-
// TODO: use our revert UI once we support range-select for reverts
14-
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15-
{
16-
Key: "X",
17-
Context: "commits",
18-
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
19-
},
20-
}
21-
},
12+
SetupConfig: func(cfg *config.AppConfig) {},
2213
SetupRepo: func(shell *Shell) {
2314
shell.CreateFileAndAdd("myfile", "")
2415
shell.Commit("add empty file")
@@ -38,13 +29,18 @@ var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArg
3829
Contains("CI ◯ unrelated change"),
3930
Contains("CI ◯ add empty file"),
4031
).
41-
Press("X").
32+
SelectNextItem().
33+
Press(keys.Universal.RangeSelectDown).
34+
Press(keys.Commits.RevertCommit).
4235
Tap(func() {
43-
t.ExpectPopup().Alert().
44-
Title(Equals("Error")).
45-
// The exact error message is different on different git versions,
46-
// but they all contain the word 'conflict' somewhere.
47-
Content(Contains("conflict")).
36+
t.ExpectPopup().Confirmation().
37+
Title(Equals("Revert commit")).
38+
Content(Equals("Are you sure you want to revert the selected commits?")).
39+
Confirm()
40+
41+
t.ExpectPopup().Menu().
42+
Title(Equals("Conflicts!")).
43+
Select(Contains("View conflicts")).
4844
Confirm()
4945
}).
5046
Lines(
@@ -59,7 +55,7 @@ var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArg
5955
t.Views().Options().Content(Contains("View revert options: m"))
6056
t.Views().Information().Content(Contains("Reverting (Reset)"))
6157

62-
t.Views().Files().Focus().
58+
t.Views().Files().IsFocused().
6359
Lines(
6460
Contains("UU myfile").IsSelected(),
6561
).

Diff for: pkg/integration/tests/interactive_rebase/revert_during_rebase_when_stopped_on_edit.go

+21-18
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,10 @@ var RevertDuringRebaseWhenStoppedOnEdit = NewIntegrationTest(NewIntegrationTestA
99
Description: "Revert a series of commits while stopped in a rebase",
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
12-
SetupConfig: func(cfg *config.AppConfig) {
13-
// TODO: use our revert UI once we support range-select for reverts
14-
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15-
{
16-
Key: "X",
17-
Context: "commits",
18-
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
19-
},
20-
}
21-
},
12+
SetupConfig: func(cfg *config.AppConfig) {},
2213
SetupRepo: func(shell *Shell) {
23-
shell.EmptyCommit("master commit")
14+
shell.EmptyCommit("master commit 1")
15+
shell.EmptyCommit("master commit 2")
2416
shell.NewBranch("branch")
2517
shell.CreateNCommits(4)
2618
},
@@ -32,7 +24,8 @@ var RevertDuringRebaseWhenStoppedOnEdit = NewIntegrationTest(NewIntegrationTestA
3224
Contains("commit 03"),
3325
Contains("commit 02"),
3426
Contains("commit 01"),
35-
Contains("master commit"),
27+
Contains("master commit 2"),
28+
Contains("master commit 1"),
3629
).
3730
NavigateToLine(Contains("commit 03")).
3831
Press(keys.Universal.Edit).
@@ -41,17 +34,27 @@ var RevertDuringRebaseWhenStoppedOnEdit = NewIntegrationTest(NewIntegrationTestA
4134
Contains("<-- YOU ARE HERE --- commit 03").IsSelected(),
4235
Contains("commit 02"),
4336
Contains("commit 01"),
44-
Contains("master commit"),
37+
Contains("master commit 2"),
38+
Contains("master commit 1"),
4539
).
46-
Press("X").
40+
SelectNextItem().
41+
Press(keys.Universal.RangeSelectDown).
42+
Press(keys.Commits.RevertCommit).
43+
Tap(func() {
44+
t.ExpectPopup().Confirmation().
45+
Title(Equals("Revert commit")).
46+
Content(MatchesRegexp(`Are you sure you want to revert \w+?`)).
47+
Confirm()
48+
}).
4749
Lines(
4850
Contains("pick").Contains("commit 04"),
49-
Contains(`<-- YOU ARE HERE --- Revert "commit 01"`).IsSelected(),
51+
Contains(`<-- YOU ARE HERE --- Revert "commit 01"`),
5052
Contains(`Revert "commit 02"`),
5153
Contains("commit 03"),
52-
Contains("commit 02"),
53-
Contains("commit 01"),
54-
Contains("master commit"),
54+
Contains("commit 02").IsSelected(),
55+
Contains("commit 01").IsSelected(),
56+
Contains("master commit 2"),
57+
Contains("master commit 1"),
5558
)
5659
},
5760
})

Diff for: pkg/integration/tests/interactive_rebase/revert_multiple_commits_in_interactive_rebase.go

+13-17
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,7 @@ var RevertMultipleCommitsInInteractiveRebase = NewIntegrationTest(NewIntegration
99
Description: "Reverts a range of commits, the first of which conflicts, in the middle of an interactive rebase",
1010
ExtraCmdArgs: []string{},
1111
Skip: false,
12-
SetupConfig: func(cfg *config.AppConfig) {
13-
// TODO: use our revert UI once we support range-select for reverts
14-
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15-
{
16-
Key: "X",
17-
Context: "commits",
18-
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
19-
},
20-
}
21-
},
12+
SetupConfig: func(cfg *config.AppConfig) {},
2213
SetupRepo: func(shell *Shell) {
2314
shell.CreateFileAndAdd("myfile", "")
2415
shell.Commit("add empty file")
@@ -44,13 +35,18 @@ var RevertMultipleCommitsInInteractiveRebase = NewIntegrationTest(NewIntegration
4435
).
4536
NavigateToLine(Contains("add second line")).
4637
Press(keys.Universal.Edit).
47-
Press("X").
38+
SelectNextItem().
39+
Press(keys.Universal.RangeSelectDown).
40+
Press(keys.Commits.RevertCommit).
4841
Tap(func() {
49-
t.ExpectPopup().Alert().
50-
Title(Equals("Error")).
51-
// The exact error message is different on different git versions,
52-
// but they all contain the word 'conflict' somewhere.
53-
Content(Contains("conflict")).
42+
t.ExpectPopup().Confirmation().
43+
Title(Equals("Revert commit")).
44+
Content(Equals("Are you sure you want to revert the selected commits?")).
45+
Confirm()
46+
47+
t.ExpectPopup().Menu().
48+
Title(Equals("Conflicts!")).
49+
Select(Contains("View conflicts")).
5450
Confirm()
5551
}).
5652
Lines(
@@ -67,7 +63,7 @@ var RevertMultipleCommitsInInteractiveRebase = NewIntegrationTest(NewIntegration
6763
t.Views().Options().Content(Contains("View revert options: m"))
6864
t.Views().Information().Content(Contains("Reverting (Reset)"))
6965

70-
t.Views().Files().Focus().
66+
t.Views().Files().IsFocused().
7167
Lines(
7268
Contains("UU myfile").IsSelected(),
7369
).

0 commit comments

Comments
 (0)