jesseduffield.lazygit/pkg/gui/controllers/helpers/merge_conflicts_helper.go
Stefan Haller bc9fafff02 Make the conflict marker size a parameter of our marker matching
Git doesn't always write conflict markers of seven characters: the
conflict-marker-size gitattribute overrides that per file, and it is set
for good reasons — for file types whose regular content tends to contain
marker-looking lines, such as documentation about merging, or test
scripts. We hard-code seven characters everywhere we look for markers,
so none of that works.

Prepare for honoring the attribute by threading the marker size through
everything that recognizes a marker, carried on the file model. Nothing
fills it in yet, so we still use git's default size of seven everywhere,
and matching is unchanged: a marker consists of exactly that many marker
characters, and all but the "=======" one are followed by a space and a
label.
2026-08-08 12:43:42 +02:00

143 lines
3.7 KiB
Go

package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type MergeConflictsHelper struct {
c *HelperCommon
}
func NewMergeConflictsHelper(
c *HelperCommon,
) *MergeConflictsHelper {
return &MergeConflictsHelper{
c: c,
}
}
func (self *MergeConflictsHelper) SetMergeState(file *models.File) (bool, error) {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()
return self.setMergeStateWithoutLock(file.Path, file.ConflictMarkerSize)
}
func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string, markerSize int) (bool, error) {
content, err := self.c.Git().File.Cat(path)
if err != nil {
return false, err
}
if path != self.context().GetState().GetPath() {
self.context().SetUserScrolling(false)
}
self.context().GetState().SetContent(content, path, markerSize)
return !self.context().GetState().NoConflicts(), nil
}
func (self *MergeConflictsHelper) ResetMergeState() {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()
self.resetMergeState()
}
func (self *MergeConflictsHelper) resetMergeState() {
self.context().SetUserScrolling(false)
self.context().GetState().Reset()
}
// EscapeMerge returns from the merge conflicts view to the files context. It
// must be called on the UI thread, without the merge-conflicts mutex held:
// pushing the files context renders the newly focused file to the main view,
// which can take the mutex again (via SetMergeState).
func (self *MergeConflictsHelper) EscapeMerge() {
self.ResetMergeState()
// The files refresh may already have opened the prompt to continue the
// rebase/merge on top of us (if all conflicts are resolved); in that case
// don't push the files context over it.
if self.c.Context().IsCurrent(self.c.Contexts().MergeConflicts) {
self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{})
}
}
// SetConflictsAndRender re-reads the file being merged and re-renders the
// merge conflicts view. Returns whether the file still has conflicts.
func (self *MergeConflictsHelper) SetConflictsAndRender() (bool, error) {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()
state := self.context().GetState()
hasConflicts, err := self.setMergeStateWithoutLock(state.GetPath(), state.GetMarkerSize())
if err != nil {
return false, err
}
if hasConflicts {
return true, self.context().Render()
}
return false, nil
}
func (self *MergeConflictsHelper) SwitchToMerge(file *models.File) error {
if self.context().GetState().GetPath() != file.Path {
hasConflicts, err := self.SetMergeState(file)
if err != nil {
return err
}
if !hasConflicts {
return nil
}
}
self.c.Context().Push(self.c.Contexts().MergeConflicts, types.OnFocusOpts{})
return nil
}
func (self *MergeConflictsHelper) context() *context.MergeConflictsContext {
return self.c.Contexts().MergeConflicts
}
func (self *MergeConflictsHelper) Render() {
content := self.context().GetContentToRender()
var task types.UpdateTask
if self.context().IsUserScrolling() {
task = types.NewRenderStringWithoutScrollTask(content)
} else {
originY := self.context().GetOriginY()
task = types.NewRenderStringWithScrollTask(content, 0, originY)
}
self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().MergeConflicts,
Main: &types.ViewUpdateOpts{
Task: task,
},
})
}
func (self *MergeConflictsHelper) RefreshMergeState() error {
if self.c.Context().Current().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY {
return nil
}
hasConflicts, err := self.SetConflictsAndRender()
if err != nil {
return err
}
if !hasConflicts {
self.EscapeMerge()
}
return nil
}