mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Add command to find base commit for creating a fixup
This commit is contained in:
		| @@ -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' | ||||
|   | ||||
							
								
								
									
										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>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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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>: Игнорировать или исключить файл | ||||
|   | ||||
| @@ -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>: 忽略文件 | ||||
|   | ||||
| @@ -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>: 忽略或排除檔案 | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										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() | ||||
| } | ||||
|  | ||||
| // 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"). | ||||
|   | ||||
| @@ -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(), | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -64,6 +64,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types | ||||
| 			Handler:     self.c.Helpers().WorkingTree.HandleCommitEditorPress, | ||||
| 			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), | ||||
|   | ||||
							
								
								
									
										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 | ||||
| 	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{}, | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										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.CreateTag, | ||||
| 	commit.DiscardOldFileChange, | ||||
| 	commit.FindBaseCommitForFixup, | ||||
| 	commit.Highlight, | ||||
| 	commit.History, | ||||
| 	commit.HistoryComplex, | ||||
|   | ||||
| @@ -914,6 +914,10 @@ | ||||
|               "type": "string", | ||||
|               "default": "C" | ||||
|             }, | ||||
|             "findBaseCommitForFixup": { | ||||
|               "type": "string", | ||||
|               "default": "\u003cc-f\u003e" | ||||
|             }, | ||||
|             "confirmDiscard": { | ||||
|               "type": "string", | ||||
|               "default": "x" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user