mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Add no-ff merge option
This will put whatever git's default merge variant is as the first menu item, and add a second item which is the opposite (no-ff if the default is ff, and vice versa). If users prefer to always have the same option first no matter whether it's applicable, they can make ff always appear first by setting git's "merge.ff" config to "true" or "only", or by setting lazygit's "git.merging.args" config to "--ff" or "--ff-only"; if they want no-ff to appear first, they can do that by setting git's "merge.ff" config to "false", or by setting lazygit's "git.merging.args" config to "--no-ff". Which of these they choose depends on whether they want the config to also apply to other git clients including the cli, or only to lazygit.
This commit is contained in:
		| @@ -285,6 +285,16 @@ func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error | ||||
| 	return self.cmd.New(cmdArgs).Run() | ||||
| } | ||||
|  | ||||
| // Returns whether refName can be fast-forward merged into the current branch | ||||
| func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool { | ||||
| 	cmdArgs := NewGitCmd("merge-base"). | ||||
| 		Arg("--is-ancestor"). | ||||
| 		Arg("HEAD", refName). | ||||
| 		ToArgv() | ||||
| 	err := self.cmd.New(cmdArgs).DontLog().Run() | ||||
| 	return err == nil | ||||
| } | ||||
|  | ||||
| // Only choose between non-empty, non-identical commands | ||||
| func (self *BranchCommands) allBranchesLogCandidates() []string { | ||||
| 	return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds)) | ||||
|   | ||||
| @@ -97,6 +97,10 @@ func (self *ConfigCommands) GetRebaseUpdateRefs() bool { | ||||
| 	return self.gitConfig.GetBool("rebase.updateRefs") | ||||
| } | ||||
|  | ||||
| func (self *ConfigCommands) GetMergeFF() string { | ||||
| 	return self.gitConfig.Get("merge.ff") | ||||
| } | ||||
|  | ||||
| func (self *ConfigCommands) DropConfigCache() { | ||||
| 	self.gitConfig.DropCache() | ||||
| } | ||||
|   | ||||
| @@ -381,21 +381,86 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e | ||||
| 		return errors.New(self.c.Tr.CantMergeBranchIntoItself) | ||||
| 	} | ||||
|  | ||||
| 	wantFastForward, wantNonFastForward := self.fastForwardMergeUserPreference() | ||||
| 	canFastForward := self.c.Git().Branch.CanDoFastForwardMerge(refName) | ||||
|  | ||||
| 	var firstRegularMergeItem *types.MenuItem | ||||
| 	var secondRegularMergeItem *types.MenuItem | ||||
| 	var fastForwardMergeItem *types.MenuItem | ||||
|  | ||||
| 	if !wantNonFastForward && (wantFastForward || canFastForward) { | ||||
| 		firstRegularMergeItem = &types.MenuItem{ | ||||
| 			Label:   self.c.Tr.RegularMergeFastForward, | ||||
| 			OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR), | ||||
| 			Key:     'm', | ||||
| 			Tooltip: utils.ResolvePlaceholderString( | ||||
| 				self.c.Tr.RegularMergeFastForwardTooltip, | ||||
| 				map[string]string{ | ||||
| 					"checkedOutBranch": checkedOutBranchName, | ||||
| 					"selectedBranch":   refName, | ||||
| 				}, | ||||
| 			), | ||||
| 		} | ||||
| 		fastForwardMergeItem = firstRegularMergeItem | ||||
|  | ||||
| 		secondRegularMergeItem = &types.MenuItem{ | ||||
| 			Label:   self.c.Tr.RegularMergeNonFastForward, | ||||
| 			OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_NON_FAST_FORWARD), | ||||
| 			Key:     'n', | ||||
| 			Tooltip: utils.ResolvePlaceholderString( | ||||
| 				self.c.Tr.RegularMergeNonFastForwardTooltip, | ||||
| 				map[string]string{ | ||||
| 					"checkedOutBranch": checkedOutBranchName, | ||||
| 					"selectedBranch":   refName, | ||||
| 				}, | ||||
| 			), | ||||
| 		} | ||||
| 	} else { | ||||
| 		firstRegularMergeItem = &types.MenuItem{ | ||||
| 			Label:   self.c.Tr.RegularMergeNonFastForward, | ||||
| 			OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR), | ||||
| 			Key:     'm', | ||||
| 			Tooltip: utils.ResolvePlaceholderString( | ||||
| 				self.c.Tr.RegularMergeNonFastForwardTooltip, | ||||
| 				map[string]string{ | ||||
| 					"checkedOutBranch": checkedOutBranchName, | ||||
| 					"selectedBranch":   refName, | ||||
| 				}, | ||||
| 			), | ||||
| 		} | ||||
|  | ||||
| 		secondRegularMergeItem = &types.MenuItem{ | ||||
| 			Label:   self.c.Tr.RegularMergeFastForward, | ||||
| 			OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_FAST_FORWARD), | ||||
| 			Key:     'f', | ||||
| 			Tooltip: utils.ResolvePlaceholderString( | ||||
| 				self.c.Tr.RegularMergeFastForwardTooltip, | ||||
| 				map[string]string{ | ||||
| 					"checkedOutBranch": checkedOutBranchName, | ||||
| 					"selectedBranch":   refName, | ||||
| 				}, | ||||
| 			), | ||||
| 		} | ||||
| 		fastForwardMergeItem = secondRegularMergeItem | ||||
| 	} | ||||
|  | ||||
| 	if !canFastForward { | ||||
| 		fastForwardMergeItem.DisabledReason = &types.DisabledReason{ | ||||
| 			Text: utils.ResolvePlaceholderString( | ||||
| 				self.c.Tr.CannotFastForwardMerge, | ||||
| 				map[string]string{ | ||||
| 					"checkedOutBranch": checkedOutBranchName, | ||||
| 					"selectedBranch":   refName, | ||||
| 				}, | ||||
| 			), | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return self.c.Menu(types.CreateMenuOptions{ | ||||
| 		Title: self.c.Tr.Merge, | ||||
| 		Items: []*types.MenuItem{ | ||||
| 			{ | ||||
| 				Label:   self.c.Tr.RegularMerge, | ||||
| 				OnPress: self.RegularMerge(refName), | ||||
| 				Key:     'm', | ||||
| 				Tooltip: utils.ResolvePlaceholderString( | ||||
| 					self.c.Tr.RegularMergeTooltip, | ||||
| 					map[string]string{ | ||||
| 						"checkedOutBranch": checkedOutBranchName, | ||||
| 						"selectedBranch":   refName, | ||||
| 					}, | ||||
| 				), | ||||
| 			}, | ||||
| 			firstRegularMergeItem, | ||||
| 			secondRegularMergeItem, | ||||
| 			{ | ||||
| 				Label:   self.c.Tr.SquashMergeUncommitted, | ||||
| 				OnPress: self.SquashMergeUncommitted(refName), | ||||
| @@ -423,10 +488,10 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (self *MergeAndRebaseHelper) RegularMerge(refName string) func() error { | ||||
| func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error { | ||||
| 	return func() error { | ||||
| 		self.c.LogAction(self.c.Tr.Actions.Merge) | ||||
| 		err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_REGULAR) | ||||
| 		err := self.c.Git().Branch.Merge(refName, variant) | ||||
| 		return self.CheckMergeOrRebase(err) | ||||
| 	} | ||||
| } | ||||
| @@ -459,6 +524,31 @@ func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranch | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Returns wantsFastForward, wantsNonFastForward. These will never both be true, but they can both be false. | ||||
| func (self *MergeAndRebaseHelper) fastForwardMergeUserPreference() (bool, bool) { | ||||
| 	// Check user config first, because it takes precedence over git config | ||||
| 	mergingArgs := self.c.UserConfig().Git.Merging.Args | ||||
| 	if strings.Contains(mergingArgs, "--ff") { // also covers "--ff-only" | ||||
| 		return true, false | ||||
| 	} | ||||
|  | ||||
| 	if strings.Contains(mergingArgs, "--no-ff") { | ||||
| 		return false, true | ||||
| 	} | ||||
|  | ||||
| 	// Then check git config | ||||
| 	mergeFfConfig := self.c.Git().Config.GetMergeFF() | ||||
| 	if mergeFfConfig == "true" || mergeFfConfig == "only" { | ||||
| 		return true, false | ||||
| 	} | ||||
|  | ||||
| 	if mergeFfConfig == "false" { | ||||
| 		return false, true | ||||
| 	} | ||||
|  | ||||
| 	return false, false | ||||
| } | ||||
|  | ||||
| func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error { | ||||
| 	self.c.Modes().MarkedBaseCommit.Reset() | ||||
| 	self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) | ||||
|   | ||||
| @@ -264,8 +264,11 @@ type TranslationSet struct { | ||||
| 	FocusMainView                         string | ||||
| 	Merge                                 string | ||||
| 	MergeBranchTooltip                    string | ||||
| 	RegularMerge                          string | ||||
| 	RegularMergeTooltip                   string | ||||
| 	RegularMergeFastForward               string | ||||
| 	RegularMergeFastForwardTooltip        string | ||||
| 	CannotFastForwardMerge                string | ||||
| 	RegularMergeNonFastForward            string | ||||
| 	RegularMergeNonFastForwardTooltip     string | ||||
| 	SquashMergeUncommitted                string | ||||
| 	SquashMergeUncommittedTooltip         string | ||||
| 	SquashMergeCommitted                  string | ||||
| @@ -1351,8 +1354,11 @@ func EnglishTranslationSet() *TranslationSet { | ||||
| 		FocusMainView:                        "Focus main view", | ||||
| 		Merge:                                `Merge`, | ||||
| 		MergeBranchTooltip:                   "View options for merging the selected item into the current branch (regular merge, squash merge)", | ||||
| 		RegularMerge:                         "Regular merge", | ||||
| 		RegularMergeTooltip:                  "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'.", | ||||
| 		RegularMergeFastForward:              "Regular merge (fast-forward)", | ||||
| 		RegularMergeFastForwardTooltip:       "Fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}' without creating a merge commit.", | ||||
| 		CannotFastForwardMerge:               "Cannot fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}'", | ||||
| 		RegularMergeNonFastForward:           "Regular merge (with merge commit)", | ||||
| 		RegularMergeNonFastForwardTooltip:    "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}', creating a merge commit.", | ||||
| 		SquashMergeUncommitted:               "Squash merge and leave uncommitted", | ||||
| 		SquashMergeUncommittedTooltip:        "Squash merge '{{.selectedBranch}}' into the working tree.", | ||||
| 		SquashMergeCommitted:                 "Squash merge and commit", | ||||
|   | ||||
							
								
								
									
										68
									
								
								pkg/integration/tests/branch/merge_fast_forward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								pkg/integration/tests/branch/merge_fast_forward.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package branch | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| ) | ||||
|  | ||||
| var MergeFastForward = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Merge a branch into another using fast-forward merge", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig: func(config *config.AppConfig) { | ||||
| 		config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical" | ||||
| 	}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		shell.NewBranch("original-branch"). | ||||
| 			EmptyCommit("one"). | ||||
| 			NewBranch("branch1"). | ||||
| 			EmptyCommit("branch1"). | ||||
| 			Checkout("original-branch"). | ||||
| 			NewBranchFrom("branch2", "original-branch"). | ||||
| 			EmptyCommit("branch2"). | ||||
| 			Checkout("original-branch") | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		t.Views().Branches(). | ||||
| 			Focus(). | ||||
| 			Lines( | ||||
| 				Contains("original-branch").IsSelected(), | ||||
| 				Contains("branch1"), | ||||
| 				Contains("branch2"), | ||||
| 			). | ||||
| 			SelectNextItem(). | ||||
| 			Press(keys.Branches.MergeIntoCurrentBranch) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Merge")). | ||||
| 			TopLines( | ||||
| 				Contains("Regular merge (fast-forward)"), | ||||
| 				Contains("Regular merge (with merge commit)"), | ||||
| 			). | ||||
| 			Select(Contains("Regular merge (fast-forward)")). | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Views().Commits(). | ||||
| 			Lines( | ||||
| 				Contains("branch1").IsSelected(), | ||||
| 				Contains("one"), | ||||
| 			) | ||||
|  | ||||
| 		// Check that branch2 can't be merged using fast-forward | ||||
| 		t.Views().Branches(). | ||||
| 			Focus(). | ||||
| 			NavigateToLine(Contains("branch2")). | ||||
| 			Press(keys.Branches.MergeIntoCurrentBranch) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Merge")). | ||||
| 			TopLines( | ||||
| 				Contains("Regular merge (with merge commit)"), | ||||
| 				Contains("Regular merge (fast-forward)"), | ||||
| 			). | ||||
| 			Select(Contains("Regular merge (fast-forward)")). | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.ExpectToast(Contains("Cannot fast-forward 'original-branch' to 'branch2'")) | ||||
| 	}, | ||||
| }) | ||||
							
								
								
									
										76
									
								
								pkg/integration/tests/branch/merge_non_fast_forward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								pkg/integration/tests/branch/merge_non_fast_forward.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package branch | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| ) | ||||
|  | ||||
| var MergeNonFastForward = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Merge a branch into another using non-fast-forward merge", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig: func(config *config.AppConfig) { | ||||
| 		config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical" | ||||
| 	}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		shell.NewBranch("original-branch"). | ||||
| 			EmptyCommit("one"). | ||||
| 			NewBranch("branch1"). | ||||
| 			EmptyCommit("branch1"). | ||||
| 			Checkout("original-branch"). | ||||
| 			NewBranchFrom("branch2", "original-branch"). | ||||
| 			EmptyCommit("branch2"). | ||||
| 			Checkout("original-branch") | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		t.Views().Branches(). | ||||
| 			Focus(). | ||||
| 			Lines( | ||||
| 				Contains("original-branch").IsSelected(), | ||||
| 				Contains("branch1"), | ||||
| 				Contains("branch2"), | ||||
| 			). | ||||
| 			SelectNextItem(). | ||||
| 			Press(keys.Branches.MergeIntoCurrentBranch) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Merge")). | ||||
| 			TopLines( | ||||
| 				Contains("Regular merge (fast-forward)"), | ||||
| 				Contains("Regular merge (with merge commit)"), | ||||
| 			). | ||||
| 			Select(Contains("Regular merge (with merge commit)")). | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Views().Commits(). | ||||
| 			Lines( | ||||
| 				Contains("⏣─╮ Merge branch 'branch1' into original-branch").IsSelected(), | ||||
| 				Contains("│ ◯ * branch1"), | ||||
| 				Contains("◯─╯ one"), | ||||
| 			) | ||||
|  | ||||
| 		// Check that branch2 shows the non-fast-forward option first | ||||
| 		t.Views().Branches(). | ||||
| 			Focus(). | ||||
| 			NavigateToLine(Contains("branch2")). | ||||
| 			Press(keys.Branches.MergeIntoCurrentBranch) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Merge")). | ||||
| 			TopLines( | ||||
| 				Contains("Regular merge (with merge commit)"), | ||||
| 				Contains("Regular merge (fast-forward)"), | ||||
| 			). | ||||
| 			Select(Contains("Regular merge (with merge commit)")). | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Views().Commits(). | ||||
| 			Lines( | ||||
| 				Contains("⏣─╮ Merge branch 'branch2' into original-branch").IsSelected(), | ||||
| 				Contains("│ ◯ * branch2"), | ||||
| 				Contains("⏣─│─╮ Merge branch 'branch1' into original-branch"), | ||||
| 				Contains("│ │ ◯ * branch1"), | ||||
| 				Contains("◯─┴─╯ one"), | ||||
| 			) | ||||
| 	}, | ||||
| }) | ||||
| @@ -48,6 +48,8 @@ var tests = []*components.IntegrationTest{ | ||||
| 	branch.DeleteRemoteBranchWithDifferentName, | ||||
| 	branch.DeleteWhileFiltering, | ||||
| 	branch.DetachedHead, | ||||
| 	branch.MergeFastForward, | ||||
| 	branch.MergeNonFastForward, | ||||
| 	branch.MoveCommitsToNewBranchFromBaseBranch, | ||||
| 	branch.MoveCommitsToNewBranchFromMainBranch, | ||||
| 	branch.MoveCommitsToNewBranchKeepStacked, | ||||
|   | ||||
| @@ -103,7 +103,7 @@ var ModeSpecificKeybindingSuggestions = NewIntegrationTest(NewIntegrationTestArg | ||||
| 			Tap(func() { | ||||
| 				t.ExpectPopup().Menu(). | ||||
| 					Title(Equals("Merge")). | ||||
| 					Select(Contains("Regular merge")). | ||||
| 					Select(Contains("Regular merge (with merge commit)")). | ||||
| 					Confirm() | ||||
|  | ||||
| 				t.Common().AcknowledgeConflicts() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user