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:
parent
4f6ae62e79
commit
7653ff9a24
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
163
files_panel.go
163
files_panel.go
@ -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)
|
||||
}
|
||||
|
@ -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
123
gui.go
@ -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
213
merge_panel.go
Normal 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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user