diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 040df0070..59c2c6833 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -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), diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 53a6b205f..f8717f413 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -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 { diff --git a/pkg/integration/tests/commit/add_co_author_range.go b/pkg/integration/tests/commit/add_co_author_range.go new file mode 100644 index 000000000..9452c1ded --- /dev/null +++ b/pkg/integration/tests/commit/add_co_author_range.go @@ -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 "). + Confirm() + }). + // exit range selection mode + PressEscape(). + SelectNextItem() + + t.Views().Main().Content( + Contains("fourth commit"). + DoesNotContain("Co-authored-by: John Smith "), + ) + + 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 "), + ) + + 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 "), + ) + + 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 "), + ) + }, +}) diff --git a/pkg/integration/tests/commit/reset_author_range.go b/pkg/integration/tests/commit/reset_author_range.go new file mode 100644 index 000000000..9d7c764cf --- /dev/null +++ b/pkg/integration/tests/commit/reset_author_range.go @@ -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"), + ) + }, +}) diff --git a/pkg/integration/tests/commit/set_author_range.go b/pkg/integration/tests/commit/set_author_range.go new file mode 100644 index 000000000..e366ba05e --- /dev/null +++ b/pkg/integration/tests/commit/set_author_range.go @@ -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 "). + Confirm() + }). + PressEscape(). + Lines( + Contains("BS").Contains("first"), + Contains("JS").Contains("second"), + Contains("JS").Contains("third").IsSelected(), + Contains("BS").Contains("fourth"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d4ebd4026..6386146ba 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -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,