mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-11-24 08:52:21 +02:00
Add command to find base commit for creating a fixup
This commit is contained in:
parent
33f933ba21
commit
8ca78412ac
@ -209,6 +209,7 @@ keybinding:
|
|||||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||||
amendLastCommit: 'A'
|
amendLastCommit: 'A'
|
||||||
commitChangesWithEditor: 'C'
|
commitChangesWithEditor: 'C'
|
||||||
|
findBaseCommitForFixup: '<c-f>'
|
||||||
confirmDiscard: 'x'
|
confirmDiscard: 'x'
|
||||||
ignoreFile: 'i'
|
ignoreFile: 'i'
|
||||||
refreshFiles: 'r'
|
refreshFiles: 'r'
|
||||||
|
64
docs/Fixup_Commits.md
Normal file
64
docs/Fixup_Commits.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Fixup Commits
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
There's this common scenario that you have a PR in review, the reviewer is
|
||||||
|
requesting some changes, and you make those changes and would normally simply
|
||||||
|
squash them into the original commit that they came from. If you do that,
|
||||||
|
however, there's no way for the reviewer to see what you changed. You could just
|
||||||
|
make a separate commit with those changes at the end of the branch, but this is
|
||||||
|
not ideal because it results in a git history that is not very clean.
|
||||||
|
|
||||||
|
To help with this, git has a concept of fixup commits: you do make a separate
|
||||||
|
commit, but the subject of this commit is the string "fixup! " followed by the
|
||||||
|
original commit subject. This both tells the reviewer what's going on (you are
|
||||||
|
making a change that you later will squash into the designated commit), and it
|
||||||
|
provides an easy way to actually perform this squash operation when you are
|
||||||
|
ready to do that (before merging).
|
||||||
|
|
||||||
|
## Creating fixup commits
|
||||||
|
|
||||||
|
You could of course create fixup commits manually by typing in the commit
|
||||||
|
message with the prefix yourself. But lazygit has an easier way to do that:
|
||||||
|
in the Commits view, select the commit that you want to create a fixup for, and
|
||||||
|
press shift-F (for "Create fixup commit for this commit"). This automatically
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Squashing fixup commits
|
||||||
|
|
||||||
|
When you're ready to merge the branch and want to squash all these fixup commits
|
||||||
|
that you created, that's very easy to do: select the first commit of your branch
|
||||||
|
and hit shift-S (for "Squash all 'fixup!' commits above selected commit
|
||||||
|
(autosquash)"). Boom, done.
|
||||||
|
|
||||||
|
## Finding the commit to create a fixup for
|
||||||
|
|
||||||
|
When you are making changes to code that you changed earlier in a long branch,
|
||||||
|
it can be tedious to find the commit to squash it into. Lazygit has a command to
|
||||||
|
help you with this, too: in the Files view, press ctrl-f to select the right
|
||||||
|
base commit in the Commits view automatically. From there, you can either press
|
||||||
|
shift-F to create a fixup commit for it, or shift-A to amend your changes into
|
||||||
|
the commit if you haven't published your branch yet.
|
||||||
|
|
||||||
|
This command works in many cases, and when it does it almost feels like magic,
|
||||||
|
but it's important to understand its limitations because it doesn't always work.
|
||||||
|
The way it works is that it looks at the deleted lines of your current
|
||||||
|
modifications, blames them to find out which commit those lines come from, and
|
||||||
|
if they all come from the same commit, it selects it. So here are cases where it
|
||||||
|
doesn't work:
|
||||||
|
|
||||||
|
- Your current diff has only added lines, but no deleted lines. In this case
|
||||||
|
there's no way for lazygit to know which commit you want to add them to.
|
||||||
|
- The deleted lines belong to multiple different commits. In this case you can
|
||||||
|
help lazygit by staging a set of files or hunks that all belong to the same
|
||||||
|
commit; if some changes are staged, the ctrl-f command works only on those.
|
||||||
|
- The found commit is already on master; in this case, lazygit refuses to select
|
||||||
|
it, because it doesn't make sense to create fixups for it, let alone amend to
|
||||||
|
it.
|
||||||
|
|
||||||
|
To sum it up: the command works great if you are changing code again that you
|
||||||
|
changed or added earlier in the same branch. This is a common enough case to
|
||||||
|
make the command useful.
|
@ -123,6 +123,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||||
<kbd>A</kbd>: Amend last commit
|
<kbd>A</kbd>: Amend last commit
|
||||||
<kbd>C</kbd>: Commit changes using git editor
|
<kbd>C</kbd>: Commit changes using git editor
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: Edit file
|
<kbd>e</kbd>: Edit file
|
||||||
<kbd>o</kbd>: Open file
|
<kbd>o</kbd>: Open file
|
||||||
<kbd>i</kbd>: Ignore or exclude file
|
<kbd>i</kbd>: Ignore or exclude file
|
||||||
|
@ -196,6 +196,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
||||||
<kbd>A</kbd>: 最新のコミットにamend
|
<kbd>A</kbd>: 最新のコミットにamend
|
||||||
<kbd>C</kbd>: gitエディタを使用して変更をコミット
|
<kbd>C</kbd>: gitエディタを使用して変更をコミット
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: ファイルを編集
|
<kbd>e</kbd>: ファイルを編集
|
||||||
<kbd>o</kbd>: ファイルを開く
|
<kbd>o</kbd>: ファイルを開く
|
||||||
<kbd>i</kbd>: ファイルをignore
|
<kbd>i</kbd>: ファイルをignore
|
||||||
|
@ -336,6 +336,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: Commit changes without pre-commit hook
|
<kbd>w</kbd>: Commit changes without pre-commit hook
|
||||||
<kbd>A</kbd>: 마지맛 커밋 수정
|
<kbd>A</kbd>: 마지맛 커밋 수정
|
||||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: 파일 편집
|
<kbd>e</kbd>: 파일 편집
|
||||||
<kbd>o</kbd>: 파일 닫기
|
<kbd>o</kbd>: 파일 닫기
|
||||||
<kbd>i</kbd>: Ignore file
|
<kbd>i</kbd>: Ignore file
|
||||||
|
@ -56,6 +56,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
|
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
|
||||||
<kbd>A</kbd>: Wijzig laatste commit
|
<kbd>A</kbd>: Wijzig laatste commit
|
||||||
<kbd>C</kbd>: Commit veranderingen met de git editor
|
<kbd>C</kbd>: Commit veranderingen met de git editor
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: Verander bestand
|
<kbd>e</kbd>: Verander bestand
|
||||||
<kbd>o</kbd>: Open bestand
|
<kbd>o</kbd>: Open bestand
|
||||||
<kbd>i</kbd>: Ignore or exclude file
|
<kbd>i</kbd>: Ignore or exclude file
|
||||||
|
@ -157,6 +157,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
|
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
|
||||||
<kbd>A</kbd>: Zmień ostatni commit
|
<kbd>A</kbd>: Zmień ostatni commit
|
||||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: Edytuj plik
|
<kbd>e</kbd>: Edytuj plik
|
||||||
<kbd>o</kbd>: Otwórz plik
|
<kbd>o</kbd>: Otwórz plik
|
||||||
<kbd>i</kbd>: Ignore or exclude file
|
<kbd>i</kbd>: Ignore or exclude file
|
||||||
|
@ -330,6 +330,7 @@ _Связки клавиш_
|
|||||||
<kbd>w</kbd>: Закоммитить изменения без предварительного хука коммита
|
<kbd>w</kbd>: Закоммитить изменения без предварительного хука коммита
|
||||||
<kbd>A</kbd>: Правка последнего коммита
|
<kbd>A</kbd>: Правка последнего коммита
|
||||||
<kbd>C</kbd>: Сохранить изменения с помощью редактора git
|
<kbd>C</kbd>: Сохранить изменения с помощью редактора git
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: Редактировать файл
|
<kbd>e</kbd>: Редактировать файл
|
||||||
<kbd>o</kbd>: Открыть файл
|
<kbd>o</kbd>: Открыть файл
|
||||||
<kbd>i</kbd>: Игнорировать или исключить файл
|
<kbd>i</kbd>: Игнорировать или исключить файл
|
||||||
|
@ -204,6 +204,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
|||||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||||
<kbd>A</kbd>: 修补最后一次提交
|
<kbd>A</kbd>: 修补最后一次提交
|
||||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: 编辑文件
|
<kbd>e</kbd>: 编辑文件
|
||||||
<kbd>o</kbd>: 打开文件
|
<kbd>o</kbd>: 打开文件
|
||||||
<kbd>i</kbd>: 忽略文件
|
<kbd>i</kbd>: 忽略文件
|
||||||
|
@ -299,6 +299,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
|
|||||||
<kbd>w</kbd>: 沒有預提交 hook 就提交更改
|
<kbd>w</kbd>: 沒有預提交 hook 就提交更改
|
||||||
<kbd>A</kbd>: 修正上次提交
|
<kbd>A</kbd>: 修正上次提交
|
||||||
<kbd>C</kbd>: 使用 git 編輯器提交變更
|
<kbd>C</kbd>: 使用 git 編輯器提交變更
|
||||||
|
<kbd><c-f></kbd>: Find base commit for fixup
|
||||||
<kbd>e</kbd>: 編輯檔案
|
<kbd>e</kbd>: 編輯檔案
|
||||||
<kbd>o</kbd>: 開啟檔案
|
<kbd>o</kbd>: 開啟檔案
|
||||||
<kbd>i</kbd>: 忽略或排除檔案
|
<kbd>i</kbd>: 忽略或排除檔案
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
// GitCommand is our main git interface
|
// GitCommand is our main git interface
|
||||||
type GitCommand struct {
|
type GitCommand struct {
|
||||||
|
Blame *git_commands.BlameCommands
|
||||||
Branch *git_commands.BranchCommands
|
Branch *git_commands.BranchCommands
|
||||||
Commit *git_commands.CommitCommands
|
Commit *git_commands.CommitCommands
|
||||||
Config *git_commands.ConfigCommands
|
Config *git_commands.ConfigCommands
|
||||||
@ -160,6 +161,7 @@ func NewGitCommandAux(
|
|||||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||||
|
blameCommands := git_commands.NewBlameCommands(gitCommon)
|
||||||
|
|
||||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||||
@ -171,6 +173,7 @@ func NewGitCommandAux(
|
|||||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||||
|
|
||||||
return &GitCommand{
|
return &GitCommand{
|
||||||
|
Blame: blameCommands,
|
||||||
Branch: branchCommands,
|
Branch: branchCommands,
|
||||||
Commit: commitCommands,
|
Commit: commitCommands,
|
||||||
Config: configCommands,
|
Config: configCommands,
|
||||||
|
33
pkg/commands/git_commands/blame.go
Normal file
33
pkg/commands/git_commands/blame.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlameCommands struct {
|
||||||
|
*GitCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlameCommands(gitCommon *GitCommon) *BlameCommands {
|
||||||
|
return &BlameCommands{
|
||||||
|
GitCommon: gitCommon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blame a range of lines. For each line, output the hash of the commit where
|
||||||
|
// the line last changed, then a space, then a description of the commit (author
|
||||||
|
// and date), another space, and then the line. For example:
|
||||||
|
//
|
||||||
|
// ac90ebac688fe8bc2ffd922157a9d2c54681d2aa (Stefan Haller 2023-08-01 14:54:56 +0200 11) func NewBlameCommands(gitCommon *GitCommon) *BlameCommands {
|
||||||
|
// ac90ebac688fe8bc2ffd922157a9d2c54681d2aa (Stefan Haller 2023-08-01 14:54:56 +0200 12) return &BlameCommands{
|
||||||
|
// ac90ebac688fe8bc2ffd922157a9d2c54681d2aa (Stefan Haller 2023-08-01 14:54:56 +0200 13) GitCommon: gitCommon,
|
||||||
|
func (self *BlameCommands) BlameLineRange(filename string, commit string, firstLine int, numLines int) (string, error) {
|
||||||
|
cmdArgs := NewGitCmd("blame").
|
||||||
|
Arg("-l").
|
||||||
|
Arg(fmt.Sprintf("-L%d,+%d", firstLine, numLines)).
|
||||||
|
Arg(commit).
|
||||||
|
Arg("--").
|
||||||
|
Arg(filename)
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs.ToArgv()).RunWithOutput()
|
||||||
|
}
|
@ -198,6 +198,20 @@ func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, e
|
|||||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Example output:
|
||||||
|
//
|
||||||
|
// cd50c79ae Preserve the commit message correctly even if the description has blank lines
|
||||||
|
// 3ebba5f32 Add test demonstrating a bug with preserving the commit message
|
||||||
|
// 9a423c388 Remove unused function
|
||||||
|
func (self *CommitCommands) GetShasAndCommitMessagesFirstLine(shas []string) (string, error) {
|
||||||
|
cmdArgs := NewGitCmd("show").
|
||||||
|
Arg("--no-patch", "--pretty=format:%h %s").
|
||||||
|
Arg(shas...).
|
||||||
|
ToArgv()
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
|
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
|
||||||
cmdArgs := NewGitCmd("show").
|
cmdArgs := NewGitCmd("show").
|
||||||
Arg("--no-patch", "--oneline").
|
Arg("--no-patch", "--oneline").
|
||||||
|
@ -78,3 +78,11 @@ func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands
|
|||||||
Arg("--", opts.Filepath).
|
Arg("--", opts.Filepath).
|
||||||
ToArgv())
|
ToArgv())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) oscommands.ICmdObj {
|
||||||
|
return self.cmd.New(
|
||||||
|
NewGitCmd("diff-index").
|
||||||
|
Arg("--submodule", "--no-ext-diff", "--no-color", "--patch").
|
||||||
|
Arg(diffArgs...).ToArgv(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -366,6 +366,7 @@ type KeybindingFilesConfig struct {
|
|||||||
CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"`
|
CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"`
|
||||||
AmendLastCommit string `yaml:"amendLastCommit"`
|
AmendLastCommit string `yaml:"amendLastCommit"`
|
||||||
CommitChangesWithEditor string `yaml:"commitChangesWithEditor"`
|
CommitChangesWithEditor string `yaml:"commitChangesWithEditor"`
|
||||||
|
FindBaseCommitForFixup string `yaml:"findBaseCommitForFixup"`
|
||||||
ConfirmDiscard string `yaml:"confirmDiscard"`
|
ConfirmDiscard string `yaml:"confirmDiscard"`
|
||||||
IgnoreFile string `yaml:"ignoreFile"`
|
IgnoreFile string `yaml:"ignoreFile"`
|
||||||
RefreshFiles string `yaml:"refreshFiles"`
|
RefreshFiles string `yaml:"refreshFiles"`
|
||||||
@ -762,6 +763,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
CommitChangesWithoutHook: "w",
|
CommitChangesWithoutHook: "w",
|
||||||
AmendLastCommit: "A",
|
AmendLastCommit: "A",
|
||||||
CommitChangesWithEditor: "C",
|
CommitChangesWithEditor: "C",
|
||||||
|
FindBaseCommitForFixup: "<c-f>",
|
||||||
IgnoreFile: "i",
|
IgnoreFile: "i",
|
||||||
RefreshFiles: "r",
|
RefreshFiles: "r",
|
||||||
StashAllChanges: "s",
|
StashAllChanges: "s",
|
||||||
|
@ -103,6 +103,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
CherryPick: cherryPickHelper,
|
CherryPick: cherryPickHelper,
|
||||||
Upstream: helpers.NewUpstreamHelper(helperCommon, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
|
Upstream: helpers.NewUpstreamHelper(helperCommon, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
|
||||||
AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper),
|
AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper),
|
||||||
|
FixupHelper: helpers.NewFixupHelper(helperCommon),
|
||||||
Commits: commitsHelper,
|
Commits: commitsHelper,
|
||||||
Snake: helpers.NewSnakeHelper(helperCommon),
|
Snake: helpers.NewSnakeHelper(helperCommon),
|
||||||
Diff: diffHelper,
|
Diff: diffHelper,
|
||||||
|
@ -64,6 +64,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||||||
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
|
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
|
||||||
Description: self.c.Tr.CommitChangesWithEditor,
|
Description: self.c.Tr.CommitChangesWithEditor,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Files.FindBaseCommitForFixup),
|
||||||
|
Handler: self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress,
|
||||||
|
Description: self.c.Tr.FindBaseCommitForFixup,
|
||||||
|
Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Key: opts.GetKey(opts.Config.Universal.Edit),
|
Key: opts.GetKey(opts.Config.Universal.Edit),
|
||||||
Handler: self.checkSelectedFileNode(self.edit),
|
Handler: self.checkSelectedFileNode(self.edit),
|
||||||
|
178
pkg/gui/controllers/helpers/fixup_helper.go
Normal file
178
pkg/gui/controllers/helpers/fixup_helper.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/generics/set"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FixupHelper struct {
|
||||||
|
c *HelperCommon
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFixupHelper(
|
||||||
|
c *HelperCommon,
|
||||||
|
) *FixupHelper {
|
||||||
|
return &FixupHelper{
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type deletedLineInfo struct {
|
||||||
|
filename string
|
||||||
|
startLineIdx int
|
||||||
|
numLines int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||||
|
diff, hasStagedChanges, err := self.getDiff()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if diff == "" {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.NoChangedFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedLineInfos := self.parseDiff(diff)
|
||||||
|
if len(deletedLineInfos) == 0 {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.NoDeletedLinesInDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
shas := self.blameDeletedLines(deletedLineInfos)
|
||||||
|
|
||||||
|
if len(shas) == 0 {
|
||||||
|
// This should never happen
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.NoBaseCommitsFound)
|
||||||
|
}
|
||||||
|
if len(shas) > 1 {
|
||||||
|
subjects, err := self.c.Git().Commit.GetShasAndCommitMessagesFirstLine(shas)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message := lo.Ternary(hasStagedChanges,
|
||||||
|
self.c.Tr.MultipleBaseCommitsFoundStaged,
|
||||||
|
self.c.Tr.MultipleBaseCommitsFoundUnstaged)
|
||||||
|
return self.c.ErrorMsg(message + "\n\n" + subjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
|
||||||
|
return commit.Sha == shas[0]
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
commits := self.c.Model().Commits
|
||||||
|
if commits[len(commits)-1].Status == models.StatusMerged {
|
||||||
|
// If the commit is not found, it's most likely because it's already
|
||||||
|
// merged, and more than 300 commits away. Check if the last known
|
||||||
|
// commit is already merged; if so, show the "already merged" error.
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||||
|
}
|
||||||
|
// If we get here, the current branch must have more then 300 commits. Unlikely...
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsNotInCurrentView)
|
||||||
|
}
|
||||||
|
if commit.Status == models.StatusMerged {
|
||||||
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !useIndex {
|
||||||
|
if err := self.c.Git().WorkingTree.StageAll(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c.Contexts().LocalCommits.SetSelectedLineIdx(index)
|
||||||
|
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FixupHelper) getDiff() (string, bool, error) {
|
||||||
|
args := []string{"-U0", "--ignore-submodules=all", "HEAD", "--"}
|
||||||
|
|
||||||
|
// Try staged changes first
|
||||||
|
hasStagedChanges := true
|
||||||
|
diff, err := self.c.Git().Diff.DiffIndexCmdObj(append([]string{"--cached"}, args...)...).RunWithOutput()
|
||||||
|
|
||||||
|
if err == nil && diff == "" {
|
||||||
|
hasStagedChanges = false
|
||||||
|
// If there are no staged changes, try unstaged changes
|
||||||
|
diff, err = self.c.Git().Diff.DiffIndexCmdObj(args...).RunWithOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff, hasStagedChanges, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FixupHelper) parseDiff(diff string) []*deletedLineInfo {
|
||||||
|
lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n")
|
||||||
|
|
||||||
|
deletedLineInfos := []*deletedLineInfo{}
|
||||||
|
|
||||||
|
hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`)
|
||||||
|
|
||||||
|
var filename string
|
||||||
|
var currentLineInfo *deletedLineInfo
|
||||||
|
finishHunk := func() {
|
||||||
|
if currentLineInfo != nil && currentLineInfo.numLines > 0 {
|
||||||
|
deletedLineInfos = append(deletedLineInfos, currentLineInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "diff --git") {
|
||||||
|
finishHunk()
|
||||||
|
currentLineInfo = nil
|
||||||
|
} else if strings.HasPrefix(line, "--- ") {
|
||||||
|
// For some reason, the line ends with a tab character if the file
|
||||||
|
// name contains spaces
|
||||||
|
filename = strings.TrimRight(line[6:], "\t")
|
||||||
|
} else if strings.HasPrefix(line, "@@ ") {
|
||||||
|
finishHunk()
|
||||||
|
match := hunkHeaderRegexp.FindStringSubmatch(line)
|
||||||
|
startIdx := utils.MustConvertToInt(match[1])
|
||||||
|
currentLineInfo = &deletedLineInfo{filename, startIdx, 0}
|
||||||
|
} else if currentLineInfo != nil && line[0] == '-' {
|
||||||
|
currentLineInfo.numLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finishHunk()
|
||||||
|
|
||||||
|
return deletedLineInfos
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the list of commit hashes that introduced the lines which have now been deleted
|
||||||
|
func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) []string {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
shaChan := make(chan string)
|
||||||
|
|
||||||
|
for _, info := range deletedLineInfos {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(info *deletedLineInfo) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
blameOutput, err := self.c.Git().Blame.BlameLineRange(info.filename, "HEAD", info.startLineIdx, info.numLines)
|
||||||
|
if err != nil {
|
||||||
|
self.c.Log.Errorf("Error blaming file '%s': %v", info.filename, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n")
|
||||||
|
for _, line := range blameLines {
|
||||||
|
shaChan <- strings.Split(line, " ")[0]
|
||||||
|
}
|
||||||
|
}(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(shaChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
result := set.New[string]()
|
||||||
|
for sha := range shaChan {
|
||||||
|
result.Add(sha)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToSlice()
|
||||||
|
}
|
@ -33,6 +33,7 @@ type Helpers struct {
|
|||||||
GPG *GpgHelper
|
GPG *GpgHelper
|
||||||
Upstream *UpstreamHelper
|
Upstream *UpstreamHelper
|
||||||
AmendHelper *AmendHelper
|
AmendHelper *AmendHelper
|
||||||
|
FixupHelper *FixupHelper
|
||||||
Commits *CommitsHelper
|
Commits *CommitsHelper
|
||||||
Snake *SnakeHelper
|
Snake *SnakeHelper
|
||||||
// lives in context package because our contexts need it to render to main
|
// lives in context package because our contexts need it to render to main
|
||||||
@ -70,6 +71,7 @@ func NewStubHelpers() *Helpers {
|
|||||||
GPG: &GpgHelper{},
|
GPG: &GpgHelper{},
|
||||||
Upstream: &UpstreamHelper{},
|
Upstream: &UpstreamHelper{},
|
||||||
AmendHelper: &AmendHelper{},
|
AmendHelper: &AmendHelper{},
|
||||||
|
FixupHelper: &FixupHelper{},
|
||||||
Commits: &CommitsHelper{},
|
Commits: &CommitsHelper{},
|
||||||
Snake: &SnakeHelper{},
|
Snake: &SnakeHelper{},
|
||||||
Diff: &DiffHelper{},
|
Diff: &DiffHelper{},
|
||||||
|
@ -39,6 +39,14 @@ type TranslationSet struct {
|
|||||||
SureToAmend string
|
SureToAmend string
|
||||||
NoCommitToAmend string
|
NoCommitToAmend string
|
||||||
CommitChangesWithEditor string
|
CommitChangesWithEditor string
|
||||||
|
FindBaseCommitForFixup string
|
||||||
|
FindBaseCommitForFixupTooltip string
|
||||||
|
NoDeletedLinesInDiff string
|
||||||
|
NoBaseCommitsFound string
|
||||||
|
MultipleBaseCommitsFoundStaged string
|
||||||
|
MultipleBaseCommitsFoundUnstaged string
|
||||||
|
BaseCommitIsAlreadyOnMainBranch string
|
||||||
|
BaseCommitIsNotInCurrentView string
|
||||||
StatusTitle string
|
StatusTitle string
|
||||||
GlobalTitle string
|
GlobalTitle string
|
||||||
Menu string
|
Menu string
|
||||||
@ -858,6 +866,14 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
SureToAmend: "Are you sure you want to amend last commit? Afterwards, you can change the commit message from the commits panel.",
|
SureToAmend: "Are you sure you want to amend last commit? Afterwards, you can change the commit message from the commits panel.",
|
||||||
NoCommitToAmend: "There's no commit to amend.",
|
NoCommitToAmend: "There's no commit to amend.",
|
||||||
CommitChangesWithEditor: "Commit changes using git editor",
|
CommitChangesWithEditor: "Commit changes using git editor",
|
||||||
|
FindBaseCommitForFixup: "Find base commit for fixup",
|
||||||
|
FindBaseCommitForFixupTooltip: "Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md>",
|
||||||
|
NoDeletedLinesInDiff: "No deleted lines in diff",
|
||||||
|
NoBaseCommitsFound: "No base commits found",
|
||||||
|
MultipleBaseCommitsFoundStaged: "Multiple base commits found. (Try staging fewer changes at once)",
|
||||||
|
MultipleBaseCommitsFoundUnstaged: "Multiple base commits found. (Try staging some of the changes)",
|
||||||
|
BaseCommitIsAlreadyOnMainBranch: "The base commit for this change is already on the main branch",
|
||||||
|
BaseCommitIsNotInCurrentView: "Base commit is not in current view",
|
||||||
StatusTitle: "Status",
|
StatusTitle: "Status",
|
||||||
Menu: "Menu",
|
Menu: "Menu",
|
||||||
Execute: "Execute",
|
Execute: "Execute",
|
||||||
|
79
pkg/integration/tests/commit/find_base_commit_for_fixup.go
Normal file
79
pkg/integration/tests/commit/find_base_commit_for_fixup.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package commit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FindBaseCommitForFixup = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
|
Description: "Finds the base commit to create a fixup for",
|
||||||
|
ExtraCmdArgs: []string{},
|
||||||
|
Skip: false,
|
||||||
|
SetupConfig: func(config *config.AppConfig) {},
|
||||||
|
SetupRepo: func(shell *Shell) {
|
||||||
|
shell.NewBranch("mybranch").
|
||||||
|
EmptyCommit("1st commit").
|
||||||
|
CreateFileAndAdd("file1", "file1 content\n").
|
||||||
|
Commit("2nd commit").
|
||||||
|
CreateFileAndAdd("file2", "file2 content\n").
|
||||||
|
Commit("3rd commit").
|
||||||
|
UpdateFile("file1", "file1 changed content").
|
||||||
|
UpdateFile("file2", "file2 changed content")
|
||||||
|
},
|
||||||
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
|
t.Views().Commits().
|
||||||
|
Lines(
|
||||||
|
Contains("3rd commit"),
|
||||||
|
Contains("2nd commit"),
|
||||||
|
Contains("1st commit"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Two changes from different commits: this fails
|
||||||
|
t.Views().Files().
|
||||||
|
Focus().
|
||||||
|
Press(keys.Files.FindBaseCommitForFixup)
|
||||||
|
|
||||||
|
t.ExpectPopup().Alert().
|
||||||
|
Title(Equals("Error")).
|
||||||
|
Content(
|
||||||
|
Contains("Multiple base commits found").
|
||||||
|
Contains("2nd commit").
|
||||||
|
Contains("3rd commit"),
|
||||||
|
).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
// Stage only one of the files: this succeeds
|
||||||
|
t.Views().Files().
|
||||||
|
IsFocused().
|
||||||
|
NavigateToLine(Contains("file1")).
|
||||||
|
PressPrimaryAction().
|
||||||
|
Press(keys.Files.FindBaseCommitForFixup)
|
||||||
|
|
||||||
|
t.Views().Commits().
|
||||||
|
IsFocused().
|
||||||
|
Lines(
|
||||||
|
Contains("3rd commit"),
|
||||||
|
Contains("2nd commit").IsSelected(),
|
||||||
|
Contains("1st commit"),
|
||||||
|
).
|
||||||
|
Press(keys.Commits.AmendToCommit)
|
||||||
|
|
||||||
|
t.ExpectPopup().Confirmation().
|
||||||
|
Title(Equals("Amend commit")).
|
||||||
|
Content(Contains("Are you sure you want to amend this commit with your staged files?")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
// Now only the other file is modified (and unstaged); this works now
|
||||||
|
t.Views().Files().
|
||||||
|
Focus().
|
||||||
|
Press(keys.Files.FindBaseCommitForFixup)
|
||||||
|
|
||||||
|
t.Views().Commits().
|
||||||
|
IsFocused().
|
||||||
|
Lines(
|
||||||
|
Contains("3rd commit").IsSelected(),
|
||||||
|
Contains("2nd commit"),
|
||||||
|
Contains("1st commit"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
@ -70,6 +70,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
commit.CommitWithPrefix,
|
commit.CommitWithPrefix,
|
||||||
commit.CreateTag,
|
commit.CreateTag,
|
||||||
commit.DiscardOldFileChange,
|
commit.DiscardOldFileChange,
|
||||||
|
commit.FindBaseCommitForFixup,
|
||||||
commit.Highlight,
|
commit.Highlight,
|
||||||
commit.History,
|
commit.History,
|
||||||
commit.HistoryComplex,
|
commit.HistoryComplex,
|
||||||
|
@ -914,6 +914,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "C"
|
"default": "C"
|
||||||
},
|
},
|
||||||
|
"findBaseCommitForFixup": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "\u003cc-f\u003e"
|
||||||
|
},
|
||||||
"confirmDiscard": {
|
"confirmDiscard": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "x"
|
"default": "x"
|
||||||
|
Loading…
Reference in New Issue
Block a user