diff options
author | Stefan Haller <stefan@haller-berlin.de> | 2023-08-01 14:54:56 +0200 |
---|---|---|
committer | Stefan Haller <stefan@haller-berlin.de> | 2024-01-10 09:11:40 +0100 |
commit | 8ca78412acdc73526248ff10c332f98ceba39fd2 (patch) | |
tree | 4f0b7c7d29a807ed47a1588b31cd090612d1caf2 | |
parent | 33f933ba21164c595508ae1ddd5fd4f73193c95c (diff) |
Add command to find base commit for creating a fixup
23 files changed, 420 insertions, 0 deletions
diff --git a/docs/Config.md b/docs/Config.md index 741a2c2b9..8b37c6aaa 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -209,6 +209,7 @@ keybinding: commitChangesWithoutHook: 'w' # commit changes without pre-commit hook amendLastCommit: 'A' commitChangesWithEditor: 'C' + findBaseCommitForFixup: '<c-f>' confirmDiscard: 'x' ignoreFile: 'i' refreshFiles: 'r' diff --git a/docs/Fixup_Commits.md b/docs/Fixup_Commits.md new file mode 100644 index 000000000..1d148c546 --- /dev/null +++ b/docs/Fixup_Commits.md @@ -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. diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index f7aaa46c8..ef6299613 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -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>A</kbd>: Amend last commit <kbd>C</kbd>: Commit changes using git editor + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: Edit file <kbd>o</kbd>: Open file <kbd>i</kbd>: Ignore or exclude file diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md index a5a54b872..36389ecc1 100644 --- a/docs/keybindings/Keybindings_ja.md +++ b/docs/keybindings/Keybindings_ja.md @@ -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>A</kbd>: 最新のコミットにamend <kbd>C</kbd>: gitエディタを使用して変更をコミット + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: ファイルを編集 <kbd>o</kbd>: ファイルを開く <kbd>i</kbd>: ファイルをignore diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md index fc7291c4b..d96283192 100644 --- a/docs/keybindings/Keybindings_ko.md +++ b/docs/keybindings/Keybindings_ko.md @@ -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>A</kbd>: 마지맛 커밋 수정 <kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다. + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: 파일 편집 <kbd>o</kbd>: 파일 닫기 <kbd>i</kbd>: Ignore file diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index c2d81a06d..23bf0a477 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -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>A</kbd>: Wijzig laatste commit <kbd>C</kbd>: Commit veranderingen met de git editor + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: Verander bestand <kbd>o</kbd>: Open bestand <kbd>i</kbd>: Ignore or exclude file diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 2136a5cdf..601ec72f5 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -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>A</kbd>: Zmień ostatni commit <kbd>C</kbd>: Zatwierdź zmiany używając edytora + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: Edytuj plik <kbd>o</kbd>: Otwórz plik <kbd>i</kbd>: Ignore or exclude file diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md index ab82515ad..6277f3db8 100644 --- a/docs/keybindings/Keybindings_ru.md +++ b/docs/keybindings/Keybindings_ru.md @@ -330,6 +330,7 @@ _Связки клавиш_ <kbd>w</kbd>: Закоммитить изменения без предварительного хука коммита <kbd>A</kbd>: Правка последнего коммита <kbd>C</kbd>: Сохранить изменения с помощью редактора git + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: Редактировать файл <kbd>o</kbd>: Открыть файл <kbd>i</kbd>: Игнорировать или исключить файл diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md index 49bce5ece..4b4a8e028 100644 --- a/docs/keybindings/Keybindings_zh-CN.md +++ b/docs/keybindings/Keybindings_zh-CN.md @@ -204,6 +204,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_ <kbd>w</kbd>: 提交更改而无需预先提交钩子 <kbd>A</kbd>: 修补最后一次提交 <kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息) + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: 编辑文件 <kbd>o</kbd>: 打开文件 <kbd>i</kbd>: 忽略文件 diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md index 201f79555..8fd6a274b 100644 --- a/docs/keybindings/Keybindings_zh-TW.md +++ b/docs/keybindings/Keybindings_zh-TW.md @@ -299,6 +299,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_ <kbd>w</kbd>: 沒有預提交 hook 就提交更改 <kbd>A</kbd>: 修正上次提交 <kbd>C</kbd>: 使用 git 編輯器提交變更 + <kbd><c-f></kbd>: Find base commit for fixup <kbd>e</kbd>: 編輯檔案 <kbd>o</kbd>: 開啟檔案 <kbd>i</kbd>: 忽略或排除檔案 diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 510661034..b1b04a72f 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -21,6 +21,7 @@ import ( // GitCommand is our main git interface type GitCommand struct { + Blame *git_commands.BlameCommands Branch *git_commands.BranchCommands Commit *git_commands.CommitCommands Config *git_commands.ConfigCommands @@ -160,6 +161,7 @@ func NewGitCommandAux( patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder) bisectCommands := git_commands.NewBisectCommands(gitCommon) worktreeCommands := git_commands.NewWorktreeCommands(gitCommon) + blameCommands := git_commands.NewBlameCommands(gitCommon) branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) @@ -171,6 +173,7 @@ func NewGitCommandAux( tagLoader := git_commands.NewTagLoader(cmn, cmd) return &GitCommand{ + Blame: blameCommands, Branch: branchCommands, Commit: commitCommands, Config: configCommands, diff --git a/pkg/commands/git_commands/blame.go b/pkg/commands/git_commands/blame.go new file mode 100644 index 000000000..aba1c63fe --- /dev/null +++ b/pkg/commands/git_commands/blame.go @@ -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() +} diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 443289f6e..e0b5b8a9a 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -198,6 +198,20 @@ func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, e 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) { cmdArgs := NewGitCmd("show"). Arg("--no-patch", "--oneline"). diff --git a/pkg/commands/git_commands/diff.go b/pkg/commands/git_commands/diff.go index 41e71e941..372939024 100644 --- a/pkg/commands/git_commands/diff.go +++ b/pkg/commands/git_commands/diff.go @@ -78,3 +78,11 @@ func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands Arg("--", opts.Filepath). 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(), + ) +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index ffc27eb88..d0d518455 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -366,6 +366,7 @@ type KeybindingFilesConfig struct { CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"` AmendLastCommit string `yaml:"amendLastCommit"` CommitChangesWithEditor string `yaml:"commitChangesWithEditor"` + FindBaseCommitForFixup string `yaml:"findBaseCommitForFixup"` ConfirmDiscard string `yaml:"confirmDiscard"` IgnoreFile string `yaml:"ignoreFile"` RefreshFiles string `yaml:"refreshFiles"` @@ -762,6 +763,7 @@ func GetDefaultConfig() *UserConfig { CommitChangesWithoutHook: "w", AmendLastCommit: "A", CommitChangesWithEditor: "C", + FindBaseCommitForFixup: "<c-f>", IgnoreFile: "i", RefreshFiles: "r", StashAllChanges: "s", diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 37c54a655..b4cbec79e 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -103,6 +103,7 @@ func (gui *Gui) resetHelpersAndControllers() { CherryPick: cherryPickHelper, Upstream: helpers.NewUpstreamHelper(helperCommon, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper), + FixupHelper: helpers.NewFixupHelper(helperCommon), Commits: commitsHelper, Snake: helpers.NewSnakeHelper(helperCommon), Diff: diffHelper, diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 0db509ca3..d60773ec1 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -65,6 +65,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types 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), Handler: self.checkSelectedFileNode(self.edit), Description: self.c.Tr.EditFile, diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go new file mode 100644 index 000000000..2198d11cb --- /dev/null +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -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() +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 9e05ba163..1f1050dc9 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -33,6 +33,7 @@ type Helpers struct { GPG *GpgHelper Upstream *UpstreamHelper AmendHelper *AmendHelper + FixupHelper *FixupHelper Commits *CommitsHelper Snake *SnakeHelper // lives in context package because our contexts need it to render to main @@ -70,6 +71,7 @@ func NewStubHelpers() *Helpers { GPG: &GpgHelper{}, Upstream: &UpstreamHelper{}, AmendHelper: &AmendHelper{}, + FixupHelper: &FixupHelper{}, Commits: &CommitsHelper{}, Snake: &SnakeHelper{}, Diff: &DiffHelper{}, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 34df4aba1..e2559f3d2 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -39,6 +39,14 @@ type TranslationSet struct { SureToAmend string NoCommitToAmend string CommitChangesWithEditor string + FindBaseCommitForFixup string + FindBaseCommitForFixupTooltip string + NoDeletedLinesInDiff string + NoBaseCommitsFound string + MultipleBaseCommitsFoundStaged string + MultipleBaseCommitsFoundUnstaged string + BaseCommitIsAlreadyOnMainBranch string + BaseCommitIsNotInCurrentView string StatusTitle string GlobalTitle 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.", NoCommitToAmend: "There's no commit to amend.", 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", Menu: "Menu", Execute: "Execute", diff --git a/pkg/integration/tests/commit/find_base_commit_for_fixup.go b/pkg/integration/tests/commit/find_base_commit_for_fixup.go new file mode 100644 index 000000000..4440932e9 --- /dev/null +++ b/pkg/integration/tests/commit/find_base_commit_for_fixup.go @@ -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"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index ccefd1dd2..c018d84e7 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -70,6 +70,7 @@ var tests = []*components.IntegrationTest{ commit.CommitWithPrefix, commit.CreateTag, commit.DiscardOldFileChange, + commit.FindBaseCommitForFixup, commit.Highlight, commit.History, commit.HistoryComplex, |