1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-27 12:32:37 +02:00

Make it easy to create "amend!" commits

To support this, we turn the confirmation prompt of the "Create fixup commit"
command into a menu; creating a fixup commit is the first entry, so that
"shift-F, enter" behaves the same as before. But there are additional entries
for creating "amend!" commits, either with or without file changes. These make
it easy to reword commit messages of existing commits.
This commit is contained in:
Stefan Haller 2024-03-18 20:21:49 +01:00
parent c92e9d9bdc
commit 150cc70698
16 changed files with 707 additions and 505 deletions

View File

@ -27,6 +27,19 @@ creates a commit with the appropriate subject line.
Don't confuse this with the lowercase "f" command ("Fixup commit"); that one Don't confuse this with the lowercase "f" command ("Fixup commit"); that one
squashes the selected commit into its parent, this is not what we want here. squashes the selected commit into its parent, this is not what we want here.
## Creating amend commits
There's a special type of fixup commit that uses "amend!" instead of "fixup!" in
the commit message subject; in addition to fixing up the original commit with
changes it allows you to also (or only) change the commit message of the
original commit. The menu that appears when pressing shift-F has options for
both of these; they bring up a commit message panel similar to when you reword a
commit, but then create the "amend!" commit containing the new message. Note
that in that panel you only type the new message as you want it to be
eventually; lazygit then takes care of formatting the "amend!" commit
appropriately for you (with the subject of your new message moving into the body
of the "amend!" commit).
## Squashing fixup commits ## Squashing fixup commits
When you're ready to merge the branch and want to squash all these fixup commits When you're ready to merge the branch and want to squash all these fixup commits

View File

@ -297,6 +297,21 @@ func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(cmdArgs).Run() return self.cmd.New(cmdArgs).Run()
} }
// CreateAmendCommit creates a commit that changes the commit message of a previous commit
func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDescription string, includeFileChanges bool) error {
description := newSubject
if newDescription != "" {
description += "\n\n" + newDescription
}
cmdArgs := NewGitCmd("commit").
Arg("-m", "amend! "+originalSubject).
Arg("-m", description).
ArgIf(!includeFileChanges, "--only", "--allow-empty").
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// a value of 0 means the head commit, 1 is the parent commit, etc // a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) { func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H"). cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").

View File

@ -180,6 +180,57 @@ func TestCommitCreateFixupCommit(t *testing.T) {
} }
} }
func TestCommitCreateAmendCommit(t *testing.T) {
type scenario struct {
testName string
originalSubject string
newSubject string
newDescription string
includeFileChanges bool
runner *oscommands.FakeCmdObjRunner
}
scenarios := []scenario{
{
testName: "subject only",
originalSubject: "original subject",
newSubject: "new subject",
newDescription: "",
includeFileChanges: true,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject"}, "", nil),
},
{
testName: "subject and description",
originalSubject: "original subject",
newSubject: "new subject",
newDescription: "new description",
includeFileChanges: true,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject\n\nnew description"}, "", nil),
},
{
testName: "without file changes",
originalSubject: "original subject",
newSubject: "new subject",
newDescription: "",
includeFileChanges: false,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject", "--only", "--allow-empty"}, "", nil),
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
err := instance.CreateAmendCommit(s.originalSubject, s.newSubject, s.newDescription, s.includeFileChanges)
assert.NoError(t, err)
s.runner.CheckForMissingCalls()
})
}
}
func TestCommitShowCmdObj(t *testing.T) { func TestCommitShowCmdObj(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string

View File

@ -832,17 +832,21 @@ func (self *LocalCommitsController) afterRevertCommit() error {
} }
func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) error { func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) error {
prompt := utils.ResolvePlaceholderString( var disabledReasonWhenFilesAreNeeded *types.DisabledReason
self.c.Tr.SureCreateFixupCommit, if len(self.c.Model().Files) == 0 {
map[string]string{ disabledReasonWhenFilesAreNeeded = &types.DisabledReason{
"commit": commit.Sha, Text: self.c.Tr.NoFilesStagedTitle,
}, ShowErrorInPanel: true,
) }
}
return self.c.Confirm(types.ConfirmOpts{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.CreateFixupCommit, Title: self.c.Tr.CreateFixupCommit,
Prompt: prompt, Items: []*types.MenuItem{
HandleConfirm: func() error { {
Label: self.c.Tr.FixupMenu_Fixup,
Key: 'f',
OnPress: func() error {
return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error { return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit) self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error { return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error {
@ -855,7 +859,60 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
}) })
}) })
}, },
DisabledReason: disabledReasonWhenFilesAreNeeded,
Tooltip: self.c.Tr.FixupMenu_FixupTooltip,
},
{
Label: self.c.Tr.FixupMenu_AmendWithChanges,
Key: 'a',
OnPress: func() error {
return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error {
return self.createAmendCommit(commit, true)
}) })
},
DisabledReason: disabledReasonWhenFilesAreNeeded,
Tooltip: self.c.Tr.FixupMenu_AmendWithChangesTooltip,
},
{
Label: self.c.Tr.FixupMenu_AmendWithoutChanges,
Key: 'r',
OnPress: func() error { return self.createAmendCommit(commit, false) },
Tooltip: self.c.Tr.FixupMenu_AmendWithoutChangesTooltip,
},
},
})
}
func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, includeFileChanges bool) error {
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Sha)
if err != nil {
return self.c.Error(err)
}
if self.c.UserConfig.Git.Commit.AutoWrapCommitMessage {
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig.Git.Commit.AutoWrapWidth)
}
originalSubject, _, _ := strings.Cut(commitMessage, "\n")
return self.c.Helpers().Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(),
InitialMessage: commitMessage,
SummaryTitle: self.c.Tr.CreateAmendCommit,
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
PreserveMessage: false,
OnConfirm: func(summary string, description string) error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error {
if err := self.c.Git().Commit.CreateAmendCommit(originalSubject, summary, description, includeFileChanges); err != nil {
return self.c.Error(err)
}
self.context().MoveSelectedLine(1)
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC})
})
},
OnSwitchToEditor: nil,
},
)
} }
func (self *LocalCommitsController) squashFixupCommits() error { func (self *LocalCommitsController) squashFixupCommits() error {

View File

@ -254,7 +254,6 @@ func chineseTranslationSet() TranslationSet {
CreateFixupCommit: `为此提交创建修正`, CreateFixupCommit: `为此提交创建修正`,
SquashAboveCommitsTooltip: `压缩在所选提交之上的所有“fixup!”提交(自动压缩)`, SquashAboveCommitsTooltip: `压缩在所选提交之上的所有“fixup!”提交(自动压缩)`,
CreateFixupCommitTooltip: `创建修正提交`, CreateFixupCommitTooltip: `创建修正提交`,
SureCreateFixupCommit: `您确定要对 {{.commit}} 创建修正提交吗?`,
ExecuteCustomCommand: "执行自定义命令", ExecuteCustomCommand: "执行自定义命令",
CustomCommand: "自定义命令:", CustomCommand: "自定义命令:",
CommitChangesWithoutHook: "提交更改而无需预先提交钩子", CommitChangesWithoutHook: "提交更改而无需预先提交钩子",

View File

@ -217,7 +217,6 @@ func dutchTranslationSet() TranslationSet {
CreateFixupCommit: `Creëer fixup commit`, CreateFixupCommit: `Creëer fixup commit`,
SquashAboveCommitsTooltip: `Squash bovenstaande commits`, SquashAboveCommitsTooltip: `Squash bovenstaande commits`,
CreateFixupCommitTooltip: `Creëer fixup commit`, CreateFixupCommitTooltip: `Creëer fixup commit`,
SureCreateFixupCommit: `Weet je zeker dat je een fixup wil maken! commit voor commit {{.commit}}?`,
ExecuteCustomCommand: "Voer aangepaste commando uit", ExecuteCustomCommand: "Voer aangepaste commando uit",
CustomCommand: "Aangepaste commando:", CustomCommand: "Aangepaste commando:",
CommitChangesWithoutHook: "Commit veranderingen zonder pre-commit hook", CommitChangesWithoutHook: "Commit veranderingen zonder pre-commit hook",

View File

@ -392,6 +392,13 @@ type TranslationSet struct {
FileResetOptionsTooltip string FileResetOptionsTooltip string
CreateFixupCommit string CreateFixupCommit string
CreateFixupCommitTooltip string CreateFixupCommitTooltip string
CreateAmendCommit string
FixupMenu_Fixup string
FixupMenu_FixupTooltip string
FixupMenu_AmendWithChanges string
FixupMenu_AmendWithChangesTooltip string
FixupMenu_AmendWithoutChanges string
FixupMenu_AmendWithoutChangesTooltip string
SquashAboveCommitsTooltip string SquashAboveCommitsTooltip string
SquashCommitsAboveSelectedTooltip string SquashCommitsAboveSelectedTooltip string
SquashCommitsInCurrentBranchTooltip string SquashCommitsInCurrentBranchTooltip string
@ -399,7 +406,6 @@ type TranslationSet struct {
SquashCommitsInCurrentBranch string SquashCommitsInCurrentBranch string
SquashCommitsAboveSelectedCommit string SquashCommitsAboveSelectedCommit string
CannotSquashCommitsInCurrentBranch string CannotSquashCommitsInCurrentBranch string
SureCreateFixupCommit string
ExecuteCustomCommand string ExecuteCustomCommand string
ExecuteCustomCommandTooltip string ExecuteCustomCommandTooltip string
CustomCommand string CustomCommand string
@ -1356,6 +1362,13 @@ func EnglishTranslationSet() TranslationSet {
FixupTooltip: "Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded.", FixupTooltip: "Meld the selected commit into the commit below it. Similar to fixup, but the selected commit's message will be discarded.",
CreateFixupCommit: "Create fixup commit", CreateFixupCommit: "Create fixup commit",
CreateFixupCommitTooltip: "Create 'fixup!' commit for the selected commit. Later on, you can press `{{.squashAbove}}` on this same commit to apply all above fixup commits.", CreateFixupCommitTooltip: "Create 'fixup!' commit for the selected commit. Later on, you can press `{{.squashAbove}}` on this same commit to apply all above fixup commits.",
CreateAmendCommit: `Create "amend!" commit`,
FixupMenu_Fixup: "fixup! commit",
FixupMenu_FixupTooltip: "Lets you fixup another commit and keep the original commit's message.",
FixupMenu_AmendWithChanges: "amend! commit with changes",
FixupMenu_AmendWithChangesTooltip: "Lets you fixup another commit and also change its commit message.",
FixupMenu_AmendWithoutChanges: "amend! commit without changes (pure reword)",
FixupMenu_AmendWithoutChangesTooltip: "Lets you change the commit message of another commit without changing its content.",
SquashAboveCommits: "Apply fixup commits", SquashAboveCommits: "Apply fixup commits",
SquashAboveCommitsTooltip: `Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash).`, SquashAboveCommitsTooltip: `Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash).`,
SquashCommitsAboveSelectedTooltip: `Squash all 'fixup!' commits above the selected commit (autosquash).`, SquashCommitsAboveSelectedTooltip: `Squash all 'fixup!' commits above the selected commit (autosquash).`,
@ -1363,7 +1376,6 @@ func EnglishTranslationSet() TranslationSet {
SquashCommitsInCurrentBranch: "In current branch", SquashCommitsInCurrentBranch: "In current branch",
SquashCommitsAboveSelectedCommit: "Above the selected commit", SquashCommitsAboveSelectedCommit: "Above the selected commit",
CannotSquashCommitsInCurrentBranch: "Cannot squash commits in current branch: the HEAD commit is a merge commit or is present on the main branch.", CannotSquashCommitsInCurrentBranch: "Cannot squash commits in current branch: the HEAD commit is a merge commit or is present on the main branch.",
SureCreateFixupCommit: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
ExecuteCustomCommand: "Execute custom command", ExecuteCustomCommand: "Execute custom command",
ExecuteCustomCommandTooltip: "Bring up a prompt where you can enter a shell command to execute. Not to be confused with pre-configured custom commands.", ExecuteCustomCommandTooltip: "Bring up a prompt where you can enter a shell command to execute. Not to be confused with pre-configured custom commands.",
CustomCommand: "Custom command:", CustomCommand: "Custom command:",

View File

@ -264,7 +264,6 @@ func japaneseTranslationSet() TranslationSet {
// LcSquashAboveCommits: `squash all 'fixup!' commits above selected commit (autosquash)`, // LcSquashAboveCommits: `squash all 'fixup!' commits above selected commit (autosquash)`,
// SquashAboveCommits: `Squash all 'fixup!' commits above selected commit (autosquash)`, // SquashAboveCommits: `Squash all 'fixup!' commits above selected commit (autosquash)`,
CreateFixupCommit: `Fixupコミットを作成`, CreateFixupCommit: `Fixupコミットを作成`,
SureCreateFixupCommit: `{{.commit}} に対する fixup! コミットを作成します。よろしいですか?`,
ExecuteCustomCommand: "カスタムコマンドを実行", ExecuteCustomCommand: "カスタムコマンドを実行",
CustomCommand: "カスタムコマンド:", CustomCommand: "カスタムコマンド:",
CommitChangesWithoutHook: "pre-commitフックを実行せずに変更をコミット", CommitChangesWithoutHook: "pre-commitフックを実行せずに変更をコミット",

View File

@ -258,7 +258,6 @@ func koreanTranslationSet() TranslationSet {
CreateFixupCommitTooltip: `Create fixup commit for this commit`, CreateFixupCommitTooltip: `Create fixup commit for this commit`,
SquashAboveCommitsTooltip: `Squash all 'fixup!' commits above selected commit (autosquash)`, SquashAboveCommitsTooltip: `Squash all 'fixup!' commits above selected commit (autosquash)`,
CreateFixupCommit: `Create fixup commit`, CreateFixupCommit: `Create fixup commit`,
SureCreateFixupCommit: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
ExecuteCustomCommand: "Execute custom command", ExecuteCustomCommand: "Execute custom command",
CustomCommand: "Custom command:", CustomCommand: "Custom command:",
CommitChangesWithoutHook: "Commit changes without pre-commit hook", CommitChangesWithoutHook: "Commit changes without pre-commit hook",

View File

@ -390,7 +390,6 @@ func polishTranslationSet() TranslationSet {
SquashCommitsAboveSelectedCommit: "Powyżej wybranego commita", SquashCommitsAboveSelectedCommit: "Powyżej wybranego commita",
CannotSquashCommitsInCurrentBranch: "Nie można scalić commitów w bieżącej gałęzi: commit HEAD jest commit merge lub jest obecny na głównej gałęzi.", CannotSquashCommitsInCurrentBranch: "Nie można scalić commitów w bieżącej gałęzi: commit HEAD jest commit merge lub jest obecny na głównej gałęzi.",
CreateFixupCommit: `Utwórz commit fixup`, CreateFixupCommit: `Utwórz commit fixup`,
SureCreateFixupCommit: `Czy na pewno chcesz utworzyć commit fixup! dla commita {{.commit}}?`,
ExecuteCustomCommand: "Wykonaj polecenie niestandardowe", ExecuteCustomCommand: "Wykonaj polecenie niestandardowe",
ExecuteCustomCommandTooltip: "Wyświetl monit, w którym możesz wprowadzić polecenie powłoki do wykonania. Nie należy mylić z wcześniej skonfigurowanymi poleceniami niestandardowymi.", ExecuteCustomCommandTooltip: "Wyświetl monit, w którym możesz wprowadzić polecenie powłoki do wykonania. Nie należy mylić z wcześniej skonfigurowanymi poleceniami niestandardowymi.",
CustomCommand: "Polecenie niestandardowe:", CustomCommand: "Polecenie niestandardowe:",

View File

@ -311,7 +311,6 @@ func RussianTranslationSet() TranslationSet {
CreateFixupCommitTooltip: `Создать fixup коммит для этого коммита`, CreateFixupCommitTooltip: `Создать fixup коммит для этого коммита`,
SquashAboveCommitsTooltip: `Объединить все 'fixup!' коммиты выше в выбранный коммит (автосохранение)`, SquashAboveCommitsTooltip: `Объединить все 'fixup!' коммиты выше в выбранный коммит (автосохранение)`,
CreateFixupCommit: `Создать fixup коммит`, CreateFixupCommit: `Создать fixup коммит`,
SureCreateFixupCommit: `Вы уверены, что хотите создать fixup! коммит для коммита {{.commit}}?`,
ExecuteCustomCommand: "Выполнить пользовательскую команду", ExecuteCustomCommand: "Выполнить пользовательскую команду",
CustomCommand: "Пользовательская Команда:", CustomCommand: "Пользовательская Команда:",
CommitChangesWithoutHook: "Закоммитить изменения без предварительного хука коммита", CommitChangesWithoutHook: "Закоммитить изменения без предварительного хука коммита",

View File

@ -340,7 +340,6 @@ func traditionalChineseTranslationSet() TranslationSet {
SquashAboveCommits: "壓縮上方所有「fixup」提交(自動壓縮)", SquashAboveCommits: "壓縮上方所有「fixup」提交(自動壓縮)",
SquashAboveCommitsTooltip: "是否壓縮上方 {{.commit}} 所有「fixup」提交?", SquashAboveCommitsTooltip: "是否壓縮上方 {{.commit}} 所有「fixup」提交?",
CreateFixupCommit: "建立修復提交", CreateFixupCommit: "建立修復提交",
SureCreateFixupCommit: "你確定要為提交{{.commit}}建立fixup!提交?",
ExecuteCustomCommand: "執行自訂命令", ExecuteCustomCommand: "執行自訂命令",
CustomCommand: "自訂命令:", CustomCommand: "自訂命令:",
CommitChangesWithoutHook: "沒有預提交 hook 就提交更改", CommitChangesWithoutHook: "沒有預提交 hook 就提交更改",

View File

@ -0,0 +1,60 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CreateAmendCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Create an amend commit for an existing commit",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(3).
CreateFileAndAdd("fixup-file", "fixup content")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToLine(Contains("commit 02")).
Press(keys.Commits.CreateFixupCommit).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Create fixup commit")).
Select(Contains("amend! commit with changes")).
Confirm()
t.ExpectPopup().CommitMessagePanel().
Content(Equals("commit 02")).
Type(" amended").Confirm()
}).
Lines(
Contains("amend! commit 02"),
Contains("commit 03"),
Contains("commit 02").IsSelected(),
Contains("commit 01"),
)
if t.Git().Version().IsAtLeast(2, 32, 0) { // Support for auto-squashing "amend!" commits was added in git 2.32.0
t.Views().Commits().
Press(keys.Commits.SquashAboveCommits).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Apply fixup commits")).
Select(Contains("Above the selected commit")).
Confirm()
}).
Lines(
Contains("commit 03"),
Contains("commit 02 amended").IsSelected(),
Contains("commit 01"),
)
}
},
})

View File

@ -26,9 +26,9 @@ var SquashFixupsAbove = NewIntegrationTest(NewIntegrationTestArgs{
NavigateToLine(Contains("commit 02")). NavigateToLine(Contains("commit 02")).
Press(keys.Commits.CreateFixupCommit). Press(keys.Commits.CreateFixupCommit).
Tap(func() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Menu().
Title(Equals("Create fixup commit")). Title(Equals("Create fixup commit")).
Content(Contains("Are you sure you want to create a fixup! commit for commit")). Select(Contains("fixup! commit")).
Confirm() Confirm()
}). }).
Lines( Lines(

View File

@ -25,9 +25,9 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
NavigateToLine(Contains("commit 01")). NavigateToLine(Contains("commit 01")).
Press(keys.Commits.CreateFixupCommit). Press(keys.Commits.CreateFixupCommit).
Tap(func() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Menu().
Title(Equals("Create fixup commit")). Title(Equals("Create fixup commit")).
Content(Contains("Are you sure you want to create a fixup! commit for commit")). Select(Contains("fixup! commit")).
Confirm() Confirm()
}). }).
NavigateToLine(Contains("commit 01").DoesNotContain("fixup!")). NavigateToLine(Contains("commit 01").DoesNotContain("fixup!")).

View File

@ -72,6 +72,7 @@ var tests = []*components.IntegrationTest{
commit.CommitSwitchToEditor, commit.CommitSwitchToEditor,
commit.CommitWipWithPrefix, commit.CommitWipWithPrefix,
commit.CommitWithPrefix, commit.CommitWithPrefix,
commit.CreateAmendCommit,
commit.CreateTag, commit.CreateTag,
commit.DiscardOldFileChanges, commit.DiscardOldFileChanges,
commit.FindBaseCommitForFixup, commit.FindBaseCommitForFixup,