1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-04 03:48:07 +02:00

add support for merging

This commit is contained in:
Jesse Duffield 2018-06-09 19:06:33 +10:00
parent 4f6ae62e79
commit 7653ff9a24
10 changed files with 540 additions and 131 deletions

View File

@ -11,7 +11,7 @@ func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g, v)
return refreshSidePanels(g)
}
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
@ -20,7 +20,7 @@ func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(branch.Name, true); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g, v)
return refreshSidePanels(g)
}, nil)
}
@ -30,20 +30,44 @@ func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
return createErrorPanel(g, output)
}
refreshSidePanels(g, v)
refreshSidePanels(g)
return handleCommitSelect(g, v)
})
return nil
}
func handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v)
defer refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot merge a branch into itself")
}
if output, err := gitMerge(selectedBranch.Name); err != nil {
return createErrorPanel(g, output)
}
return nil
}
func getSelectedBranch(v *gocui.View) Branch {
lineNumber := getItemPosition(v)
return state.Branches[lineNumber]
}
func renderBranchesOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "checkout",
"f": "force checkout",
"m": "merge",
})
}
// may want to standardise how these select methods work
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
renderString(g, "options", "space: checkout, f: force checkout")
if err := renderBranchesOptions(g); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(state.Branches) == 0 {
return renderString(g, "main", "No branches for this repo")
}

View File

@ -60,8 +60,18 @@ func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
}, nil)
}
func renderCommitsOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"s": "squash down",
"r": "rename",
"g": "reset to this commit",
})
}
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
renderString(g, "options", "s: squash down, r: rename, g: reset to this commit")
if err := renderCommitsOptions(g); err != nil {
return err
}
commit, err := getSelectedCommit(g)
if err != nil {
if err != ErrNoCommits {

View File

@ -53,7 +53,6 @@ func getMessageHeight(message string, width int) int {
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := 60
// panelHeight := int(math.Ceil(float64(len(prompt)) / float64(panelWidth)))
panelHeight := getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
@ -113,7 +112,7 @@ func setKeyBindings(g *gocui.Gui, handleYes, handleNo func(*gocui.Gui, *gocui.Vi
return nil
}
func createSimpleConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}

View File

@ -30,7 +30,7 @@ func stagedFiles(files []GitFile) []GitFile {
}
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(v)
file, err := getSelectedFile(g)
if err != nil {
return err
}
@ -51,16 +51,20 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error {
return nil
}
func getSelectedFile(v *gocui.View) (GitFile, error) {
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
if len(state.GitFiles) == 0 {
return GitFile{}, ErrNoFiles
}
lineNumber := getItemPosition(v)
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := getItemPosition(filesView)
return state.GitFiles[lineNumber], nil
}
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(v)
file, err := getSelectedFile(g)
if err != nil {
return err
}
@ -79,7 +83,7 @@ func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
}
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(v)
file, err := getSelectedFile(g)
if err != nil {
return err
}
@ -90,30 +94,52 @@ func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
return refreshFiles(g)
}
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
optionsMap := map[string]string{
"tab": "next panel",
"S": "stash files",
"c": "commit changes",
"o": "open",
"s": "open in sublime",
"i": "ignore",
"d": "delete",
"space": "toggle staged",
}
if state.HasMergeConflicts {
optionsMap["a"] = "abort merge"
optionsMap["m"] = "resolve merge conflicts"
}
if gitFile == nil {
return renderOptionsMap(g, optionsMap)
}
if gitFile.Tracked {
optionsMap["d"] = "checkout"
}
return renderOptionsMap(g, optionsMap)
}
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
baseString := "tab: next panel, S: stash files, space: toggle staged, c: commit changes, o: open, s: open in sublime, i: ignore"
item, err := getSelectedFile(v)
gitFile, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
renderString(g, "main", "No changed files")
colorLog(color.FgRed, "error")
return renderString(g, "options", baseString)
return renderfilesOptions(g, nil)
}
var optionsString string
if item.Tracked {
optionsString = baseString + ", d: checkout"
} else {
optionsString = baseString + ", d: delete"
renderfilesOptions(g, &gitFile)
var content string
if gitFile.HasMergeConflicts {
return refreshMergePanel(g)
}
renderString(g, "options", optionsString)
diff := getDiff(item)
return renderString(g, "main", diff)
content = getDiff(gitFile)
return renderString(g, "main", content)
}
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(stagedFiles(state.GitFiles)) == 0 {
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
return createErrorPanel(g, "There are no staged files to commit")
}
createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error {
@ -131,7 +157,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
}
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error {
file, err := getSelectedFile(v)
file, err := getSelectedFile(g)
if err != nil {
return err
}
@ -146,31 +172,64 @@ func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile)
}
func refreshStateGitFiles() {
// get files to stage
gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
updateHasMergeConflictStatus()
}
func updateHasMergeConflictStatus() error {
merging, err := isInMergeState()
if err != nil {
return err
}
state.HasMergeConflicts = merging
return nil
}
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !gitFile.Tracked {
red.Fprintln(filesView, gitFile.DisplayString)
return
}
green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name)
} else {
green.Fprintln(filesView, gitFile.Name)
}
}
func catSelectedFile(g *gocui.Gui) (string, error) {
item, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return "", err
}
return "", renderString(g, "main", "No file to display")
}
cat, err := catFile(item.Name)
if err != nil {
panic(err)
}
return cat, nil
}
func refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
// get files to stage
gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
refreshStateGitFiles()
filesView.Clear()
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
for _, gitFile := range state.GitFiles {
if !gitFile.Tracked {
red.Fprintln(filesView, gitFile.DisplayString)
continue
}
green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name)
} else {
green.Fprintln(filesView, gitFile.Name)
}
renderGitFile(gitFile, filesView)
}
correctCursor(filesView)
if filesView == g.CurrentView() {
@ -181,7 +240,7 @@ func refreshFiles(g *gocui.Gui) error {
func pullFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pulling...")
createSimpleConfirmationPanel(g, v, "", "Pulling...")
createMessagePanel(g, v, "", "Pulling...")
go func() {
if output, err := gitPull(); err != nil {
createErrorPanel(g, output)
@ -198,7 +257,7 @@ func pullFiles(g *gocui.Gui, v *gocui.View) error {
func pushFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pushing...")
createSimpleConfirmationPanel(g, v, "", "Pushing...")
createMessagePanel(g, v, "", "Pushing...")
go func() {
if output, err := gitPush(); err != nil {
createErrorPanel(g, output)
@ -211,3 +270,31 @@ func pushFiles(g *gocui.Gui, v *gocui.View) error {
}()
return nil
}
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
return nil
}
if !file.HasMergeConflicts {
return nil
}
switchFocus(g, v, mergeView)
return refreshMergePanel(g)
}
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
output, err := gitAbortMerge()
if err != nil {
return createErrorPanel(g, output)
}
createMessagePanel(g, v, "", "Merge aborted")
return refreshFiles(g)
}

View File

@ -21,6 +21,7 @@ type GitFile struct {
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
@ -30,7 +31,6 @@ type Branch struct {
Type string
BaseBranch string
DisplayString string
DisplayColor color.Attribute
}
// Commit : A git commit
@ -133,8 +133,8 @@ func branchPropertiesFromName(name string) (string, string, color.Attribute) {
return "other", name, color.FgWhite
}
func coloredString(str string, colour color.Attribute) string {
return color.New(colour).SprintFunc()(fmt.Sprint(str))
func coloredString(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
func withPadding(str string, padding int) string {
@ -143,17 +143,17 @@ func withPadding(str string, padding int) string {
func branchFromLine(line string, index int) Branch {
recency, name := branchStringParts(line)
branchType, branchBase, colour := branchPropertiesFromName(name)
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
if index == 0 {
recency = " *"
}
colour := color.New(colourAttr)
displayString := withPadding(recency, 4) + coloredString(name, colour)
return Branch{
Name: name,
Type: branchType,
BaseBranch: branchBase,
DisplayString: displayString,
DisplayColor: colour,
}
}
@ -206,10 +206,11 @@ func getGitStatusFiles() []GitFile {
gitFile := GitFile{
Name: filename,
DisplayString: statusString,
HasStagedChanges: tracked && stagedChange != " ",
HasStagedChanges: tracked && stagedChange != " " && stagedChange != "U",
HasUnstagedChanges: !tracked || unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: statusString[0:2] == "UU",
}
gitFiles = append(gitFiles, gitFile)
}
@ -328,14 +329,15 @@ func getDiff(file GitFile) string {
trackedArg = "--no-index /dev/null "
}
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
s, err := runCommand(command)
if err != nil {
// for now we assume an error means the file was deleted
return s
}
// for now we assume an error means the file was deleted
s, _ := runCommand(command)
return s
}
func catFile(file string) (string, error) {
return runDirectCommand("cat " + file)
}
func stageFile(file string) error {
_, err := runCommand("git add " + file)
return err
@ -350,6 +352,14 @@ func getGitStatus() (string, error) {
return runCommand("git status --untracked-files=all --short")
}
func isInMergeState() (bool, error) {
output, err := runCommand("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
func removeFile(file GitFile) error {
// if the file isn't tracked, we assume you want to delete it
if !file.Tracked {
@ -398,6 +408,14 @@ func gitListStash() (string, error) {
return runDirectCommand("git stash list")
}
func gitMerge(branchName string) (string, error) {
return runDirectCommand("git merge --no-edit " + branchName)
}
func gitAbortMerge() (string, error) {
return runDirectCommand("git merge --abort")
}
func gitUpstreamDifferenceCount() (string, string) {
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
if err != nil {

123
gui.go
View File

@ -8,22 +8,38 @@ import (
"log"
"time"
// "strings"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
)
type stateType struct {
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []conflict
EditHistory *stack.Stack
}
type conflict struct {
start int
middle int
end int
}
var state = stateType{
GitFiles: make([]GitFile, 0),
PreviousView: "files",
Commits: make([]Commit, 0),
StashEntries: make([]StashEntry, 0),
GitFiles: make([]GitFile, 0),
PreviousView: "files",
Commits: make([]Commit, 0),
StashEntries: make([]StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]conflict, 0),
EditHistory: stack.New(),
}
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
@ -44,6 +60,10 @@ func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
return nil
}
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
return refreshSidePanels(g)
}
func keybindings(g *gocui.Gui) error {
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
return err
@ -66,6 +86,15 @@ func keybindings(g *gocui.Gui) error {
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
}
@ -75,24 +104,45 @@ func keybindings(g *gocui.Gui) error {
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("", 'P', gocui.ModNone, pushFiles); err != nil {
return err
}
if err := g.SetKeybinding("", 'p', gocui.ModNone, pullFiles); err != nil {
return err
}
if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); 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
}
@ -102,6 +152,9 @@ func keybindings(g *gocui.Gui) error {
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
}
@ -127,12 +180,14 @@ func keybindings(g *gocui.Gui) error {
}
func layout(g *gocui.Gui) error {
g.Highlight = true
g.SelFgColor = gocui.AttrBold
width, height := g.Size()
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := height - 20
commitsBranchesBoundary := height - 10
commitsStashBoundary := height - 5
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
optionsTop := height - 2
// hiding options if there's not enough space
@ -241,35 +296,3 @@ func run() {
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
// const mcRide = "
// `.-::-`
// -/o+oossys+:.
// `+o++++++osssyys/`
// ://-:+.` .::-. . `++oyyo/-/+oooosyhy.
// `-+sy::-:::/+o+yss+-... /s++ss/:/:+osyoosydh`
// `-:/+o/:/+:/+-s/:s/o+`/++s++/:--/+shds+++yd:
// `y+/+soy:+/-o++y+yhyyyo/---/oyhddo/::od-
// .+o-``-+syysy//o:-oo+oyyyo+oyhyddds/oshy
// `:o++o+/-....-:/+oooyyh+:ooshhhhhhdddssyyy`
// .:o+/++ooosso//:::+yo.::hs+++:yhhhhdddhoyhh:
// `-/+so///+osyso-.:://++-` `:hhhdsohddhhhdddssh+
// -+oso++ssoyys:.` ydddddddddddhho+yd+
// `:sysssssssydh:` `-:::-..-...`ydddddddddyso++shds
// `/syyysssyyhhdd+``..://+ooo/++ssssoyddddddhho/:::oyhdhs-`
// -syyyysssyhhddhyo++++/::+/+/-:::///+sddddhs//+o+/ososyhhs+/.`
// `+hhyyyyyyyhddhs+///://///+ooo/::+o++osyhyyys+--+//o//oosyys++++:..``
// .sddhyhyyyhddyso++/::://////+syo/:osssssyhsssoooosoo//+ossssyssooooo+++:.
// .hdhhhhhhhhhysssssysssssssyyyhddso+soyhhhsssooosyyssso+syysoososoo/++osyo/
// -syyyyyyyyyyyyyyyyyyo/::----:shdsyo+yysyyyssssosyysos+/+++/+ooo++:/+/ooss/
// `........----..`` odhyyyhhsysoss++oysso++s/++++syys++/:::/:+sy-
// `ydyssyysyoyyo+sysyys++s+++++ooo+osss+/+++syy
// /dysyssoyyoo+oyyshss//:---:/++++oshhysooosyh`
// .dhhhyysyyys++yyyyss+--:::/:///oshddhhyo+osy`
// yddhhyyssy+//ssyyso/-:://+ooosyhddhsoo+/+so
// +ddhhyysss+osyyysss:::/oyyhhyhddddds+///oy/
// /dddhhyyyssysssssss+++ooyhdddddddhdyo///yyo
// /dddhyyyyyysssoo+/:-/oshhdddddddssdds+//sys
// +ddhhyyhhy/oo+/:::::+syhddddddds -hdyo++ohh`
// sddhhysyysoys/:::::osyhdddddddy` sdhsosohh:
// `dddddhhhhhhhyo:-/ossoshddddhhd- .ddyssohh/"

213
merge_panel.go Normal file
View File

@ -0,0 +1,213 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package main
import (
"bufio"
"bytes"
"io/ioutil"
"os"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func findConflicts(content string) ([]conflict, error) {
conflicts := make([]conflict, 0)
var newConflict conflict
for i, line := range splitLines(content) {
if line == "<<<<<<< HEAD" {
newConflict = conflict{start: i}
} else if line == "=======" {
newConflict.middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.end = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
}
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range splitLines(content) {
colourAttr := color.FgWhite
if i == conflict.start || i == conflict.middle || i == conflict.end {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(coloredString(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = true
return refreshMergePanel(g)
}
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = false
return refreshMergePanel(g)
}
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex >= len(state.Conflicts)-1 {
return nil
}
state.ConflictIndex++
return refreshMergePanel(g)
}
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex <= 0 {
return nil
}
state.ConflictIndex--
return refreshMergePanel(g)
}
func isIndexToDelete(i int, conflict conflict, top bool) 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)
}
func resolveConflict(filename string, conflict conflict, top bool) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !isIndexToDelete(i, conflict, top) {
output += line
}
}
devLog(output)
return ioutil.WriteFile(filename, []byte(output), 0644)
}
func pushFileSnapshot(filename string) error {
content, err := catFile(filename)
if err != nil {
return err
}
state.EditHistory.Push(content)
return nil
}
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
colorLog(color.FgCyan, "IM HERE")
if state.EditHistory.Len() == 0 {
return nil
}
prevContent := state.EditHistory.Pop().(string)
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return refreshMergePanel(g)
}
func handlePickConflict(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
pushFileSnapshot(gitFile.Name)
err = resolveConflict(gitFile.Name, conflict, state.ConflictTop)
if err != nil {
panic(err)
}
return refreshMergePanel(g)
}
func currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
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
}
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
}
return renderString(g, "main", content)
}
func switchToMerging(g *gocui.Gui) error {
state.ConflictIndex = 0
state.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return refreshMergePanel(g)
}
func renderMergeOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"up/down": "pick hunk",
"left/right": "previous/next commit",
"space": "pick hunk",
"z": "undo",
})
}
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshFiles(g)
return switchFocus(g, v, filesView)
}

View File

@ -30,8 +30,18 @@ func getSelectedStashEntry(v *gocui.View) *StashEntry {
return &state.StashEntries[lineNumber]
}
func renderStashOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "apply",
"k": "pop",
"d": "drop",
})
}
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
renderString(g, "options", "space: apply, k: pop, d: drop")
if err := renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := getSelectedStashEntry(v)
if stashEntry == nil {

View File

@ -21,6 +21,13 @@ func refreshStatus(g *gocui.Gui) error {
pushables, pullables := gitUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := state.Branches
if err := updateHasMergeConflictStatus(); err != nil {
return err
}
if state.HasMergeConflicts {
colour := color.New(color.FgYellow)
fmt.Fprint(v, coloredString(" (merging)", colour))
}
if len(branches) == 0 {
return nil
}

View File

@ -2,16 +2,15 @@ package main
import (
"fmt"
"sort"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var cyclableViews = []string{"files", "branches", "commits", "stash"}
func refreshSidePanels(g *gocui.Gui, v *gocui.View) error {
func refreshSidePanels(g *gocui.Gui) error {
refreshBranches(g)
refreshFiles(g)
refreshCommits(g)
@ -54,6 +53,9 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
case "confirmation":
return nil
case "main":
// TODO: pull this out into a 'view focused' function
refreshMergePanel(g)
v.Highlight = false
return nil
case "commits":
return handleCommitSelect(g, v)
@ -72,6 +74,7 @@ func returnFocus(g *gocui.Gui, v *gocui.View) error {
return switchFocus(g, v, previousView)
}
// pass in oldView = nil if you don't want to be able to return to your old view
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a confirmation panel i.e.
// we should never stack confirmation panels
@ -100,7 +103,9 @@ func trimmedContent(v *gocui.View) string {
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
if v == nil {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
@ -116,24 +121,20 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
return nil
}
func resetOrigin(v *gocui.View) error {
if err := v.SetCursor(0, 0); err != nil {
return err
}
return v.SetOrigin(0, 0)
}
func cursorDown(g *gocui.Gui, v *gocui.View) error {
if v != nil {
cx, cy := v.Cursor()
ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 {
return nil
}
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
cx, cy := v.Cursor()
ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 {
return nil
}
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
}
@ -141,6 +142,13 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error {
return nil
}
func resetOrigin(v *gocui.View) error {
if err := v.SetCursor(0, 0); err != nil {
return err
}
return v.SetOrigin(0, 0)
}
// if the cursor down past the last item, move it up one
func correctCursor(v *gocui.View) error {
cx, cy := v.Cursor()
@ -154,8 +162,6 @@ func correctCursor(v *gocui.View) error {
func renderString(g *gocui.Gui, viewName, s string) error {
g.Update(func(*gocui.Gui) error {
timeStart := time.Now()
colorLog(color.FgRed, viewName)
v, err := g.View(viewName)
if err != nil {
panic(err)
@ -163,7 +169,6 @@ func renderString(g *gocui.Gui, viewName, s string) error {
v.Clear()
fmt.Fprint(v, s)
v.Wrap = true
devLog("render time: ", time.Now().Sub(timeStart))
return nil
})
return nil
@ -179,3 +184,16 @@ func splitLines(multilineString string) []string {
}
return lines
}
func optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
optionsArray = append(optionsArray, key+": "+description)
}
sort.Strings(optionsArray)
return strings.Join(optionsArray, ", ")
}
func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return renderString(g, "options", optionsMapToString(optionsMap))
}