github.com/andrewrech/lazygit@v0.8.1/pkg/gui/merge_panel.go (about) 1 // though this panel is called the merge panel, it's really going to use the main panel. This may change in the future 2 3 package gui 4 5 import ( 6 "bufio" 7 "bytes" 8 "io/ioutil" 9 "math" 10 "os" 11 "strings" 12 13 "github.com/fatih/color" 14 "github.com/golang-collections/collections/stack" 15 "github.com/jesseduffield/gocui" 16 "github.com/jesseduffield/lazygit/pkg/commands" 17 "github.com/jesseduffield/lazygit/pkg/utils" 18 ) 19 20 func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) { 21 conflicts := make([]commands.Conflict, 0) 22 var newConflict commands.Conflict 23 for i, line := range utils.SplitLines(content) { 24 trimmedLine := strings.TrimPrefix(line, "++") 25 gui.Log.Info(trimmedLine) 26 if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" { 27 newConflict = commands.Conflict{Start: i} 28 } else if trimmedLine == "=======" { 29 newConflict.Middle = i 30 } else if strings.HasPrefix(trimmedLine, ">>>>>>> ") { 31 newConflict.End = i 32 conflicts = append(conflicts, newConflict) 33 } 34 } 35 return conflicts, nil 36 } 37 38 func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { 39 return conflicts[0], conflicts[1:] 40 } 41 42 func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool { 43 return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top) 44 } 45 46 func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) { 47 if len(conflicts) == 0 { 48 return content, nil 49 } 50 conflict, remainingConflicts := gui.shiftConflict(conflicts) 51 var outputBuffer bytes.Buffer 52 for i, line := range utils.SplitLines(content) { 53 colourAttr := color.FgWhite 54 if i == conflict.Start || i == conflict.Middle || i == conflict.End { 55 colourAttr = color.FgRed 56 } 57 colour := color.New(colourAttr) 58 if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) { 59 colour.Add(color.Bold) 60 } 61 if i == conflict.End && len(remainingConflicts) > 0 { 62 conflict, remainingConflicts = gui.shiftConflict(remainingConflicts) 63 } 64 outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n") 65 } 66 return outputBuffer.String(), nil 67 } 68 69 func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error { 70 gui.State.Panels.Merging.ConflictTop = true 71 return gui.refreshMergePanel() 72 } 73 74 func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error { 75 gui.State.Panels.Merging.ConflictTop = false 76 return gui.refreshMergePanel() 77 } 78 79 func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error { 80 if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 { 81 return nil 82 } 83 gui.State.Panels.Merging.ConflictIndex++ 84 return gui.refreshMergePanel() 85 } 86 87 func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { 88 if gui.State.Panels.Merging.ConflictIndex <= 0 { 89 return nil 90 } 91 gui.State.Panels.Merging.ConflictIndex-- 92 return gui.refreshMergePanel() 93 } 94 95 func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool { 96 return i == conflict.Middle || 97 i == conflict.Start || 98 i == conflict.End || 99 pick != "both" && 100 (pick == "bottom" && i > conflict.Start && i < conflict.Middle) || 101 (pick == "top" && i > conflict.Middle && i < conflict.End) 102 } 103 104 func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error { 105 gitFile, err := gui.getSelectedFile(g) 106 if err != nil { 107 return err 108 } 109 file, err := os.Open(gitFile.Name) 110 if err != nil { 111 return err 112 } 113 defer file.Close() 114 115 reader := bufio.NewReader(file) 116 output := "" 117 for i := 0; true; i++ { 118 line, err := reader.ReadString('\n') 119 if err != nil { 120 break 121 } 122 if !gui.isIndexToDelete(i, conflict, pick) { 123 output += line 124 } 125 } 126 gui.Log.Info(output) 127 return ioutil.WriteFile(gitFile.Name, []byte(output), 0644) 128 } 129 130 func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error { 131 gitFile, err := gui.getSelectedFile(g) 132 if err != nil { 133 return err 134 } 135 content, err := gui.GitCommand.CatFile(gitFile.Name) 136 if err != nil { 137 return err 138 } 139 gui.State.Panels.Merging.EditHistory.Push(content) 140 return nil 141 } 142 143 func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { 144 if gui.State.Panels.Merging.EditHistory.Len() == 0 { 145 return nil 146 } 147 prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string) 148 gitFile, err := gui.getSelectedFile(g) 149 if err != nil { 150 return err 151 } 152 ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644) 153 return gui.refreshMergePanel() 154 } 155 156 func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error { 157 conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex] 158 gui.pushFileSnapshot(g) 159 pick := "bottom" 160 if gui.State.Panels.Merging.ConflictTop { 161 pick = "top" 162 } 163 err := gui.resolveConflict(g, conflict, pick) 164 if err != nil { 165 panic(err) 166 } 167 168 // if that was the last conflict, finish the merge for this file 169 if len(gui.State.Panels.Merging.Conflicts) == 1 { 170 if err := gui.handleCompleteMerge(); err != nil { 171 return err 172 } 173 } 174 return gui.refreshMergePanel() 175 } 176 177 func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error { 178 conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex] 179 gui.pushFileSnapshot(g) 180 err := gui.resolveConflict(g, conflict, "both") 181 if err != nil { 182 panic(err) 183 } 184 return gui.refreshMergePanel() 185 } 186 187 func (gui *Gui) refreshMergePanel() error { 188 panelState := gui.State.Panels.Merging 189 cat, err := gui.catSelectedFile(gui.g) 190 if err != nil { 191 return err 192 } 193 if cat == "" { 194 return nil 195 } 196 panelState.Conflicts, err = gui.findConflicts(cat) 197 if err != nil { 198 return err 199 } 200 201 // handle potential fixes that the user made in their editor since we last refreshed 202 if len(panelState.Conflicts) == 0 { 203 return gui.handleCompleteMerge() 204 } else if panelState.ConflictIndex > len(panelState.Conflicts)-1 { 205 panelState.ConflictIndex = len(panelState.Conflicts) - 1 206 } 207 208 hasFocus := gui.currentViewName() == "main" 209 content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus) 210 if err != nil { 211 return err 212 } 213 if err := gui.renderString(gui.g, "main", content); err != nil { 214 return err 215 } 216 if err := gui.scrollToConflict(gui.g); err != nil { 217 return err 218 } 219 220 mainView := gui.getMainView() 221 mainView.Wrap = false 222 223 return nil 224 } 225 226 func (gui *Gui) scrollToConflict(g *gocui.Gui) error { 227 panelState := gui.State.Panels.Merging 228 if len(panelState.Conflicts) == 0 { 229 return nil 230 } 231 mergingView := gui.getMainView() 232 conflict := panelState.Conflicts[panelState.ConflictIndex] 233 ox, _ := mergingView.Origin() 234 _, height := mergingView.Size() 235 conflictMiddle := (conflict.End + conflict.Start) / 2 236 newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2)))) 237 gui.g.Update(func(g *gocui.Gui) error { 238 return mergingView.SetOrigin(ox, newOriginY) 239 }) 240 return nil 241 } 242 243 func (gui *Gui) renderMergeOptions() error { 244 return gui.renderOptionsMap(map[string]string{ 245 "↑ ↓": gui.Tr.SLocalize("selectHunk"), 246 "← →": gui.Tr.SLocalize("navigateConflicts"), 247 "space": gui.Tr.SLocalize("pickHunk"), 248 "b": gui.Tr.SLocalize("pickBothHunks"), 249 "z": gui.Tr.SLocalize("undo"), 250 }) 251 } 252 253 func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { 254 gui.State.Panels.Merging.EditHistory = stack.New() 255 if err := gui.refreshFiles(); err != nil { 256 return err 257 } 258 // it's possible this method won't be called from the merging view so we need to 259 // ensure we only 'return' focus if we already have it 260 if gui.g.CurrentView() == gui.getMainView() { 261 return gui.switchFocus(g, v, gui.getFilesView()) 262 } 263 return nil 264 } 265 266 func (gui *Gui) handleCompleteMerge() error { 267 if err := gui.stageSelectedFile(gui.g); err != nil { 268 return err 269 } 270 if err := gui.refreshFiles(); err != nil { 271 return err 272 } 273 // if we got conflicts after unstashing, we don't want to call any git 274 // commands to continue rebasing/merging here 275 if gui.State.WorkingTreeState == "normal" { 276 return gui.handleEscapeMerge(gui.g, gui.getMainView()) 277 } 278 // if there are no more files with merge conflicts, we should ask whether the user wants to continue 279 if !gui.anyFilesWithMergeConflicts() { 280 return gui.promptToContinue() 281 } 282 return gui.handleEscapeMerge(gui.g, gui.getMainView()) 283 } 284 285 // promptToContinue asks the user if they want to continue the rebase/merge that's in progress 286 func (gui *Gui) promptToContinue() error { 287 return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error { 288 return gui.genericMergeCommand("continue") 289 }, nil) 290 }