diff --git a/files_panel.go b/files_panel.go index f29cbb75a..dd1f85525 100644 --- a/files_panel.go +++ b/files_panel.go @@ -29,12 +29,24 @@ func stagedFiles(files []GitFile) []GitFile { return result } +func stageSelectedFile(g *gocui.Gui) error { + file, err := getSelectedFile(g) + if err != nil { + return err + } + return stageFile(file.Name) +} + func handleFilePress(g *gocui.Gui, v *gocui.View) error { file, err := getSelectedFile(g) if err != nil { return err } + if file.HasMergeConflicts { + return handleSwitchToMerge(g, v) + } + if file.HasUnstagedChanges { stageFile(file.Name) } else { @@ -64,22 +76,22 @@ func getSelectedFile(g *gocui.Gui) (GitFile, error) { } func handleFileRemove(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - var deleteVerb string - if file.Tracked { - deleteVerb = "checkout" - } else { - deleteVerb = "delete" - } - return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error { - if err := removeFile(file); err != nil { - panic(err) - } - return refreshFiles(g) - }, nil) + file, err := getSelectedFile(g) + if err != nil { + return err + } + var deleteVerb string + if file.Tracked { + deleteVerb = "checkout" + } else { + deleteVerb = "delete" + } + return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error { + if err := removeFile(file); err != nil { + panic(err) + } + return refreshFiles(g) + }, nil) } func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { @@ -176,7 +188,7 @@ func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { } func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { - return refreshFiles(g) + return refreshFiles(g) } func refreshStateGitFiles() { @@ -291,7 +303,7 @@ func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { return nil } if !file.HasMergeConflicts { - return nil + return createErrorPanel(g, "This file has no merge conflicts") } switchFocus(g, v, mergeView) return refreshMergePanel(g) diff --git a/gui.go b/gui.go index 90b177107..e01bfa7ef 100644 --- a/gui.go +++ b/gui.go @@ -66,124 +66,127 @@ func handleRefresh(g *gocui.Gui, v *gocui.View) error { } func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUpMain); err != nil { - return err - } - if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { - return err - } - if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { - return err - } - if err := g.SetKeybinding("", 'R', gocui.ModNone, handleRefresh); err != nil { - return err - } - if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil { - return err - } - if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { - return err - } - if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil { - return err - } - if err := g.SetKeybinding("files", 'm', gocui.ModNone, handleSwitchToMerge); err != nil { - return err - } - if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { - return err - } - if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { - return err - } - if err := g.SetKeybinding("files", 'r', gocui.ModNone, handleRefreshFiles); err != nil { - return err - } - if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil { - return err - } - if err := g.SetKeybinding("files", 'a', gocui.ModNone, handleAbortMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, handleSelectTop); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyEsc, gocui.ModNone, handleEscapeMerge); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, handleSelectBottom); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, handlePickConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowLeft, gocui.ModNone, handleSelectPrevConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", gocui.KeyArrowRight, gocui.ModNone, handleSelectNextConflict); err != nil { - return err - } - if err := g.SetKeybinding("main", 'z', gocui.ModNone, handlePopFileSnapshot); err != nil { - return err - } - if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'c', gocui.ModNone, handleCheckoutByName); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil { - return err - } - if err := g.SetKeybinding("branches", 'm', gocui.ModNone, handleMerge); err != nil { - return err - } - if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil { - return err - } - if err := g.SetKeybinding("commits", 'r', gocui.ModNone, handleRenameCommit); err != nil { - return err - } - if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { - return err - } - if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil { - return err - } - // TODO: come up with a better keybinding (p/P used for pushing/pulling which - // I'd like to be global. Perhaps all global keybindings should use a modifier - // like command? But then there's gonna be hotkey conflicts with the terminal - if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil { - return err - } - if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil { - return err - } - return nil + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil { + return err + } + if err := g.SetKeybinding("", 'q', gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgup, gocui.ModNone, scrollUpMain); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil { + return err + } + if err := g.SetKeybinding("", 'P', gocui.ModNone, pushFiles); err != nil { + return err + } + if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil { + return err + } + if err := g.SetKeybinding("", 'R', gocui.ModNone, handleRefresh); err != nil { + return err + } + if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil { + return err + } + if err := g.SetKeybinding("files", gocui.KeySpace, gocui.ModNone, handleFilePress); err != nil { + return err + } + if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil { + return err + } + if err := g.SetKeybinding("files", 'm', gocui.ModNone, handleSwitchToMerge); err != nil { + return err + } + if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil { + return err + } + if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil { + return err + } + if err := g.SetKeybinding("files", 'r', gocui.ModNone, handleRefreshFiles); err != nil { + return err + } + if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil { + return err + } + if err := g.SetKeybinding("files", 'a', gocui.ModNone, handleAbortMerge); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowUp, gocui.ModNone, handleSelectTop); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyEsc, gocui.ModNone, handleEscapeMerge); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowDown, gocui.ModNone, handleSelectBottom); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeySpace, gocui.ModNone, handlePickHunk); err != nil { + return err + } + if err := g.SetKeybinding("main", 'b', gocui.ModNone, handlePickBothHunks); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowLeft, gocui.ModNone, handleSelectPrevConflict); err != nil { + return err + } + if err := g.SetKeybinding("main", gocui.KeyArrowRight, gocui.ModNone, handleSelectNextConflict); err != nil { + return err + } + if err := g.SetKeybinding("main", 'z', gocui.ModNone, handlePopFileSnapshot); err != nil { + return err + } + if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'c', gocui.ModNone, handleCheckoutByName); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'F', gocui.ModNone, handleForceCheckout); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil { + return err + } + if err := g.SetKeybinding("branches", 'm', gocui.ModNone, handleMerge); err != nil { + return err + } + if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil { + return err + } + if err := g.SetKeybinding("commits", 'r', gocui.ModNone, handleRenameCommit); err != nil { + return err + } + if err := g.SetKeybinding("commits", 'g', gocui.ModNone, handleResetToCommit); err != nil { + return err + } + if err := g.SetKeybinding("stash", gocui.KeySpace, gocui.ModNone, handleStashApply); err != nil { + return err + } + // TODO: come up with a better keybinding (p/P used for pushing/pulling which + // I'd like to be global. Perhaps all global keybindings should use a modifier + // like command? But then there's gonna be hotkey conflicts with the terminal + if err := g.SetKeybinding("stash", 'k', gocui.ModNone, handleStashPop); err != nil { + return err + } + if err := g.SetKeybinding("stash", 'd', gocui.ModNone, handleStashDrop); err != nil { + return err + } + return nil } func layout(g *gocui.Gui) error { diff --git a/merge_panel.go b/merge_panel.go index c5e910a2c..f0043ee94 100644 --- a/merge_panel.go +++ b/merge_panel.go @@ -6,6 +6,7 @@ import ( "bufio" "bytes" "io/ioutil" + "math" "os" "strings" @@ -86,16 +87,21 @@ func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { return refreshMergePanel(g) } -func isIndexToDelete(i int, conflict conflict, top bool) bool { +func isIndexToDelete(i int, conflict conflict, pick string) bool { return i == conflict.middle || i == conflict.start || i == conflict.end || - (!top && i > conflict.start && i < conflict.middle) || - (top && i > conflict.middle && i < conflict.end) + pick != "both" && + (pick == "bottom" && i > conflict.start && i < conflict.middle) || + (pick == "top" && i > conflict.middle && i < conflict.end) } -func resolveConflict(filename string, conflict conflict, top bool) error { - file, err := os.Open(filename) +func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error { + gitFile, err := getSelectedFile(g) + if err != nil { + return err + } + file, err := os.Open(gitFile.Name) if err != nil { return err } @@ -108,16 +114,20 @@ func resolveConflict(filename string, conflict conflict, top bool) error { if err != nil { break } - if !isIndexToDelete(i, conflict, top) { + if !isIndexToDelete(i, conflict, pick) { output += line } } devLog(output) - return ioutil.WriteFile(filename, []byte(output), 0644) + return ioutil.WriteFile(gitFile.Name, []byte(output), 0644) } -func pushFileSnapshot(filename string) error { - content, err := catFile(filename) +func pushFileSnapshot(g *gocui.Gui) error { + gitFile, err := getSelectedFile(g) + if err != nil { + return err + } + content, err := catFile(gitFile.Name) if err != nil { return err } @@ -139,14 +149,25 @@ func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { return refreshMergePanel(g) } -func handlePickConflict(g *gocui.Gui, v *gocui.View) error { +func handlePickHunk(g *gocui.Gui, v *gocui.View) error { conflict := state.Conflicts[state.ConflictIndex] - gitFile, err := getSelectedFile(g) - if err != nil { - return err + pushFileSnapshot(g) + pick := "bottom" + if state.ConflictTop { + pick = "top" } - pushFileSnapshot(gitFile.Name) - err = resolveConflict(gitFile.Name, conflict, state.ConflictTop) + err := resolveConflict(g, conflict, pick) + if err != nil { + panic(err) + } + refreshMergePanel(g) + return nil +} + +func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error { + conflict := state.Conflicts[state.ConflictIndex] + pushFileSnapshot(g) + err := resolveConflict(g, conflict, "both") if err != nil { panic(err) } @@ -159,46 +180,48 @@ func currentViewName(g *gocui.Gui) string { } func refreshMergePanel(g *gocui.Gui) error { - cat, err := catSelectedFile(g) - if err != nil { - return err - } - state.Conflicts, err = findConflicts(cat) - if err != nil { - return err - } + cat, err := catSelectedFile(g) + if err != nil { + return err + } + state.Conflicts, err = findConflicts(cat) + if err != nil { + return err + } - if len(state.Conflicts) == 0 { - state.ConflictIndex = 0 - } else if state.ConflictIndex > len(state.Conflicts)-1 { - state.ConflictIndex = len(state.Conflicts) - 1 - } - hasFocus := currentViewName(g) == "main" - if hasFocus { - renderMergeOptions(g) - } - content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus) - if err != nil { - return err - } - if err := scrollToConflict(g); err != nil { - return err - } - return renderString(g, "main", content) + if len(state.Conflicts) == 0 { + return handleCompleteMerge(g) + } else if state.ConflictIndex > len(state.Conflicts)-1 { + state.ConflictIndex = len(state.Conflicts) - 1 + } + hasFocus := currentViewName(g) == "main" + if hasFocus { + renderMergeOptions(g) + } + content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus) + if err != nil { + return err + } + if err := scrollToConflict(g); err != nil { + return err + } + return renderString(g, "main", content) } func scrollToConflict(g *gocui.Gui) error { - mainView, err := g.View("main") - if err != nil { - return err - } - if len(state.Conflicts) == 0 { - return nil - } - conflict := state.Conflicts[state.ConflictIndex] - ox, oy := mainView.Origin() - devLog(oy, conflict.start) - return mainView.SetOrigin(ox, conflict.start) + mainView, err := g.View("main") + if err != nil { + return err + } + if len(state.Conflicts) == 0 { + return nil + } + conflict := state.Conflicts[state.ConflictIndex] + ox, _ := mainView.Origin() + _, height := mainView.Size() + conflictMiddle := (conflict.end + conflict.start) / 2 + newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2)))) + return mainView.SetOrigin(ox, newOriginY) } func switchToMerging(g *gocui.Gui) error { @@ -216,6 +239,7 @@ func renderMergeOptions(g *gocui.Gui) error { "up/down": "pick hunk", "left/right": "previous/next commit", "space": "pick hunk", + "b": "pick both hunks", "z": "undo", }) } @@ -228,3 +252,13 @@ func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { refreshFiles(g) return switchFocus(g, v, filesView) } + +func handleCompleteMerge(g *gocui.Gui) error { + filesView, err := g.View("files") + if err != nil { + return err + } + stageSelectedFile(g) + refreshFiles(g) + return switchFocus(g, nil, filesView) +} diff --git a/test/generate_basic_repo.sh b/test/generate_basic_repo.sh index 385ea8520..ef04333c7 100755 --- a/test/generate_basic_repo.sh +++ b/test/generate_basic_repo.sh @@ -18,19 +18,31 @@ cd ${reponame} git init +function add_spacing { + for i in {1..60} + do + echo "..." >> $1 + done +} + echo "Here is a story that has been told throuhg the ages" >> file1 + git add file1 git commit -m "first commit" git checkout -b develop echo "once upon a time there was a dog" >> file1 +add_spacing file1 +echo "once upon a time there was another dog" >> file1 git add file1 git commit -m "first commit on develop" git checkout master echo "once upon a time there was a cat" >> file1 +add_spacing file1 +echo "once upon a time there was another cat" >> file1 git add file1 git commit -m "first commit on develop"