1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-29 23:17:32 +02:00

Support range select for amending commit attributes (#3587)

- **PR Description**

This PR makes it possible for users to select a range of commits from
the commits view to amend their attributes (set/reset author, add
co-author), the same way it's already possible to do for a single
commit.

It closes #3273. 

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
Stefan Haller 2024-06-07 23:28:54 +02:00 committed by GitHub
commit 6cb2ac6fcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 282 additions and 42 deletions

View File

@ -67,42 +67,47 @@ func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index
}), nil
}
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
return self.GenericAmend(commits, index, func() error {
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, start, end int) error {
return self.GenericAmend(commits, start, end, func(_ *models.Commit) error {
return self.commit.ResetAuthor()
})
}
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int, value string) error {
return self.GenericAmend(commits, index, func() error {
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, start, end int, value string) error {
return self.GenericAmend(commits, start, end, func(_ *models.Commit) error {
return self.commit.SetAuthor(value)
})
}
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, index int, value string) error {
return self.GenericAmend(commits, index, func() error {
return self.commit.AddCoAuthor(commits[index].Hash, value)
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, start, end int, value string) error {
return self.GenericAmend(commits, start, end, func(commit *models.Commit) error {
return self.commit.AddCoAuthor(commit.Hash, value)
})
}
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
if models.IsHeadCommit(commits, index) {
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, start, end int, f func(commit *models.Commit) error) error {
if start == end && models.IsHeadCommit(commits, start) {
// we've selected the top commit so no rebase is required
return f()
return f(commits[start])
}
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
err := self.BeginInteractiveRebaseForCommitRange(commits, start, end, false)
if err != nil {
return err
}
// now the selected commit should be our head so we'll amend it
err = f()
if err != nil {
return err
for commitIndex := end; commitIndex >= start; commitIndex-- {
err = f(commits[commitIndex])
if err != nil {
return err
}
if err := self.ContinueRebase(); err != nil {
return err
}
}
return self.ContinueRebase()
return nil
}
func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx int, endIdx int) error {
@ -381,7 +386,13 @@ func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) er
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
commits []*models.Commit, commitIndex int, keepCommitsThatBecomeEmpty bool,
) error {
if len(commits)-1 < commitIndex {
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
}
func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
commits []*models.Commit, start, end int, keepCommitsThatBecomeEmpty bool,
) error {
if len(commits)-1 < end {
return errors.New("index outside of range of commits")
}
@ -392,14 +403,17 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
return errors.New(self.Tr.DisabledForGPG)
}
changes := []daemon.ChangeTodoAction{{
Hash: commits[commitIndex].Hash,
NewAction: todo.Edit,
}}
changes := make([]daemon.ChangeTodoAction, 0, end-start)
for commitIndex := end; commitIndex >= start; commitIndex-- {
changes = append(changes, daemon.ChangeTodoAction{
Hash: commits[commitIndex].Hash,
NewAction: todo.Edit,
})
}
self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
baseHashOrRoot: getBaseHashOrRoot(commits, end+1),
overrideEditor: true,
keepCommitsThatBecomeEmpty: keepCommitsThatBecomeEmpty,
instruction: daemon.NewChangeTodoActionsInstruction(changes),

View File

@ -238,8 +238,8 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
},
{
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
Handler: self.withItem(self.amendAttribute),
GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)),
Handler: self.withItemsRange(self.amendAttribute),
GetDisabledReason: self.require(self.itemRangeSelected(self.canAmendRange)),
Description: self.c.Tr.AmendCommitAttribute,
Tooltip: self.c.Tr.AmendCommitAttributeTooltip,
OpensMenu: true,
@ -371,7 +371,7 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
}
func (self *LocalCommitsController) switchFromCommitMessagePanelToEditor(filepath string) error {
if self.isHeadCommit() {
if self.isSelectedHeadCommit() {
return self.c.RunSubprocessAndRefresh(
self.c.Git().Commit.RewordLastCommitInEditorWithMessageFileCmdObj(filepath))
}
@ -408,7 +408,7 @@ func (self *LocalCommitsController) handleReword(summary string, description str
func (self *LocalCommitsController) doRewordEditor() error {
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
if self.isHeadCommit() {
if self.isSelectedHeadCommit() {
return self.c.RunSubprocessAndRefresh(self.c.Git().Commit.RewordLastCommitInEditorCmdObj())
}
@ -607,7 +607,7 @@ func (self *LocalCommitsController) rewordEnabled(commit *models.Commit) *types.
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if self.isRebasing() && !self.isHeadCommit() {
if self.isRebasing() && !self.isSelectedHeadCommit() {
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
}
@ -665,7 +665,7 @@ func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, sta
}
func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
if self.isHeadCommit() {
if self.isSelectedHeadCommit() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendCommitTitle,
Prompt: self.c.Tr.AmendCommitPrompt,
@ -695,34 +695,39 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
})
}
func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.DisabledReason {
if !self.isHeadCommit() && self.isRebasing() {
func (self *LocalCommitsController) canAmendRange(commits []*models.Commit, start, end int) *types.DisabledReason {
if (start != end || !self.isHeadCommit(start)) && self.isRebasing() {
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
}
return nil
}
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
func (self *LocalCommitsController) canAmend(_ *models.Commit) *types.DisabledReason {
idx := self.context().GetSelectedLineIdx()
return self.canAmendRange(self.c.Model().Commits, idx, idx)
}
func (self *LocalCommitsController) amendAttribute(commits []*models.Commit, start, end int) error {
opts := self.c.KeybindingsOpts()
return self.c.Menu(types.CreateMenuOptions{
Title: "Amend commit attribute",
Items: []*types.MenuItem{
{
Label: self.c.Tr.ResetAuthor,
OnPress: self.resetAuthor,
OnPress: func() error { return self.resetAuthor(start, end) },
Key: opts.GetKey(opts.Config.AmendAttribute.ResetAuthor),
Tooltip: self.c.Tr.ResetAuthorTooltip,
},
{
Label: self.c.Tr.SetAuthor,
OnPress: self.setAuthor,
OnPress: func() error { return self.setAuthor(start, end) },
Key: opts.GetKey(opts.Config.AmendAttribute.SetAuthor),
Tooltip: self.c.Tr.SetAuthorTooltip,
},
{
Label: self.c.Tr.AddCoAuthor,
OnPress: self.addCoAuthor,
OnPress: func() error { return self.addCoAuthor(start, end) },
Key: opts.GetKey(opts.Config.AmendAttribute.AddCoAuthor),
Tooltip: self.c.Tr.AddCoAuthorTooltip,
},
@ -730,10 +735,10 @@ func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error
})
}
func (self *LocalCommitsController) resetAuthor() error {
func (self *LocalCommitsController) resetAuthor(start, end int) error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor)
if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx()); err != nil {
if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, start, end); err != nil {
return err
}
@ -741,14 +746,14 @@ func (self *LocalCommitsController) resetAuthor() error {
})
}
func (self *LocalCommitsController) setAuthor() error {
func (self *LocalCommitsController) setAuthor(start, end int) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.SetAuthorPromptTitle,
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
HandleConfirm: func(value string) error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor)
if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil {
if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, start, end, value); err != nil {
return err
}
@ -758,14 +763,14 @@ func (self *LocalCommitsController) setAuthor() error {
})
}
func (self *LocalCommitsController) addCoAuthor() error {
func (self *LocalCommitsController) addCoAuthor(start, end int) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.AddCoAuthorPromptTitle,
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
HandleConfirm: func(value string) error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.AddCommitCoAuthor)
if err := self.c.Git().Rebase.AddCommitCoAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil {
if err := self.c.Git().Rebase.AddCommitCoAuthor(self.c.Model().Commits, start, end, value); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
@ -1188,8 +1193,12 @@ func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) erro
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
}
func (self *LocalCommitsController) isHeadCommit() bool {
return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx())
func (self *LocalCommitsController) isHeadCommit(idx int) bool {
return models.IsHeadCommit(self.c.Model().Commits, idx)
}
func (self *LocalCommitsController) isSelectedHeadCommit() bool {
return self.isHeadCommit(self.context().GetSelectedLineIdx())
}
func (self *LocalCommitsController) notMidRebase(message string) func() *types.DisabledReason {

View File

@ -0,0 +1,105 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddCoAuthorRange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add co-author on a range of commits",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("fourth commit")
shell.EmptyCommit("third commit")
shell.EmptyCommit("second commit")
shell.EmptyCommit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
Contains("second commit"),
Contains("third commit"),
Contains("fourth commit"),
).
SelectNextItem().
Press(keys.Universal.ToggleRangeSelect).
SelectNextItem().
Lines(
Contains("first commit"),
Contains("second commit").IsSelected(),
Contains("third commit").IsSelected(),
Contains("fourth commit"),
).
Press(keys.Commits.ResetCommitAuthor).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Amend commit attribute")).
Select(Contains("Add co-author")).
Confirm()
t.ExpectPopup().Prompt().
Title(Contains("Add co-author")).
Type("John Smith <jsmith@gmail.com>").
Confirm()
}).
// exit range selection mode
PressEscape().
SelectNextItem()
t.Views().Main().Content(
Contains("fourth commit").
DoesNotContain("Co-authored-by: John Smith <jsmith@gmail.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
Lines(
Contains("first commit"),
Contains("second commit"),
Contains("third commit").IsSelected(),
Contains("fourth commit"),
)
t.Views().Main().ContainsLines(
Equals(" third commit"),
Equals(" "),
Equals(" Co-authored-by: John Smith <jsmith@gmail.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
Lines(
Contains("first commit"),
Contains("second commit").IsSelected(),
Contains("third commit"),
Contains("fourth commit"),
)
t.Views().Main().ContainsLines(
Equals(" second commit"),
Equals(" "),
Equals(" Co-authored-by: John Smith <jsmith@gmail.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
Lines(
Contains("first commit").IsSelected(),
Contains("second commit"),
Contains("third commit"),
Contains("fourth commit"),
)
t.Views().Main().Content(
Contains("first commit").
DoesNotContain("Co-authored-by: John Smith <jsmith@gmail.com>"),
)
},
})

View File

@ -0,0 +1,52 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ResetAuthorRange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Reset author on a range of commits",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.SetConfig("user.email", "Bill@example.com")
shell.SetConfig("user.name", "Bill Smith")
shell.EmptyCommit("fourth")
shell.EmptyCommit("third")
shell.EmptyCommit("second")
shell.EmptyCommit("first")
shell.SetConfig("user.email", "John@example.com")
shell.SetConfig("user.name", "John Smith")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("BS").Contains("first").IsSelected(),
Contains("BS").Contains("second"),
Contains("BS").Contains("third"),
Contains("BS").Contains("fourth"),
).
SelectNextItem().
Press(keys.Universal.ToggleRangeSelect).
SelectNextItem().
Press(keys.Commits.ResetCommitAuthor).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Amend commit attribute")).
Select(Contains("Reset author")).
Confirm()
}).
PressEscape().
Lines(
Contains("BS").Contains("first"),
Contains("JS").Contains("second"),
Contains("JS").Contains("third").IsSelected(),
Contains("BS").Contains("fourth"),
)
},
})

View File

@ -0,0 +1,57 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SetAuthorRange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Set author on a range of commits",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.SetConfig("user.email", "Bill@example.com")
shell.SetConfig("user.name", "Bill Smith")
shell.EmptyCommit("fourth")
shell.EmptyCommit("third")
shell.EmptyCommit("second")
shell.EmptyCommit("first")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("BS").Contains("first").IsSelected(),
Contains("BS").Contains("second"),
Contains("BS").Contains("third"),
Contains("BS").Contains("fourth"),
)
t.Views().Commits().
Focus().
SelectNextItem().
Press(keys.Universal.ToggleRangeSelect).
SelectNextItem().
Press(keys.Commits.ResetCommitAuthor).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Amend commit attribute")).
Select(Contains(" Set author")). // adding space at start to distinguish from 'reset author'
Confirm()
t.ExpectPopup().Prompt().
Title(Contains("Set author")).
Type("John Smith <John@example.com>").
Confirm()
}).
PressEscape().
Lines(
Contains("BS").Contains("first"),
Contains("JS").Contains("second"),
Contains("JS").Contains("third").IsSelected(),
Contains("BS").Contains("fourth"),
)
},
})

View File

@ -68,6 +68,7 @@ var tests = []*components.IntegrationTest{
cherry_pick.CherryPickDuringRebase,
cherry_pick.CherryPickRange,
commit.AddCoAuthor,
commit.AddCoAuthorRange,
commit.AddCoAuthorWhileCommitting,
commit.Amend,
commit.AutoWrapMessage,
@ -89,11 +90,13 @@ var tests = []*components.IntegrationTest{
commit.NewBranch,
commit.PreserveCommitMessage,
commit.ResetAuthor,
commit.ResetAuthorRange,
commit.Revert,
commit.RevertMerge,
commit.Reword,
commit.Search,
commit.SetAuthor,
commit.SetAuthorRange,
commit.StageRangeOfLines,
commit.Staged,
commit.StagedWithoutHooks,