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
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
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()
}
// 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
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
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) {
type scenario struct {
testName string

View File

@ -832,17 +832,21 @@ func (self *LocalCommitsController) afterRevertCommit() error {
}
func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) error {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.SureCreateFixupCommit,
map[string]string{
"commit": commit.Sha,
},
)
var disabledReasonWhenFilesAreNeeded *types.DisabledReason
if len(self.c.Model().Files) == 0 {
disabledReasonWhenFilesAreNeeded = &types.DisabledReason{
Text: self.c.Tr.NoFilesStagedTitle,
ShowErrorInPanel: true,
}
}
return self.c.Confirm(types.ConfirmOpts{
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.CreateFixupCommit,
Prompt: prompt,
HandleConfirm: func() error {
Items: []*types.MenuItem{
{
Label: self.c.Tr.FixupMenu_Fixup,
Key: 'f',
OnPress: func() error {
return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
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 {

View File

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

View File

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

View File

@ -392,6 +392,13 @@ type TranslationSet struct {
FileResetOptionsTooltip string
CreateFixupCommit 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
SquashCommitsAboveSelectedTooltip string
SquashCommitsInCurrentBranchTooltip string
@ -399,7 +406,6 @@ type TranslationSet struct {
SquashCommitsInCurrentBranch string
SquashCommitsAboveSelectedCommit string
CannotSquashCommitsInCurrentBranch string
SureCreateFixupCommit string
ExecuteCustomCommand string
ExecuteCustomCommandTooltip 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.",
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.",
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",
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).`,
@ -1363,7 +1376,6 @@ func EnglishTranslationSet() TranslationSet {
SquashCommitsInCurrentBranch: "In current branch",
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.",
SureCreateFixupCommit: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
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.",
CustomCommand: "Custom command:",

View File

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

View File

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

View File

@ -390,7 +390,6 @@ func polishTranslationSet() TranslationSet {
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.",
CreateFixupCommit: `Utwórz commit fixup`,
SureCreateFixupCommit: `Czy na pewno chcesz utworzyć commit fixup! dla commita {{.commit}}?`,
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.",
CustomCommand: "Polecenie niestandardowe:",

View File

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

View File

@ -340,7 +340,6 @@ func traditionalChineseTranslationSet() TranslationSet {
SquashAboveCommits: "壓縮上方所有「fixup」提交(自動壓縮)",
SquashAboveCommitsTooltip: "是否壓縮上方 {{.commit}} 所有「fixup」提交?",
CreateFixupCommit: "建立修復提交",
SureCreateFixupCommit: "你確定要為提交{{.commit}}建立fixup!提交?",
ExecuteCustomCommand: "執行自訂命令",
CustomCommand: "自訂命令:",
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")).
Press(keys.Commits.CreateFixupCommit).
Tap(func() {
t.ExpectPopup().Confirmation().
t.ExpectPopup().Menu().
Title(Equals("Create fixup commit")).
Content(Contains("Are you sure you want to create a fixup! commit for commit")).
Select(Contains("fixup! commit")).
Confirm()
}).
Lines(

View File

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

View File

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