mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-11 04:15:48 +02:00
add support for merging
This commit is contained in:
parent
4f6ae62e79
commit
7653ff9a24
10 changed files with 540 additions and 131 deletions
|
@ -11,7 +11,7 @@ func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||||
if output, err := gitCheckout(branch.Name, false); err != nil {
|
if output, err := gitCheckout(branch.Name, false); err != nil {
|
||||||
createErrorPanel(g, output)
|
createErrorPanel(g, output)
|
||||||
}
|
}
|
||||||
return refreshSidePanels(g, v)
|
return refreshSidePanels(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
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 {
|
if output, err := gitCheckout(branch.Name, true); err != nil {
|
||||||
createErrorPanel(g, output)
|
createErrorPanel(g, output)
|
||||||
}
|
}
|
||||||
return refreshSidePanels(g, v)
|
return refreshSidePanels(g)
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,20 +30,44 @@ func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||||
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
|
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
|
||||||
return createErrorPanel(g, output)
|
return createErrorPanel(g, output)
|
||||||
}
|
}
|
||||||
refreshSidePanels(g, v)
|
refreshSidePanels(g)
|
||||||
return handleCommitSelect(g, v)
|
return handleCommitSelect(g, v)
|
||||||
})
|
})
|
||||||
return nil
|
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 {
|
func getSelectedBranch(v *gocui.View) Branch {
|
||||||
lineNumber := getItemPosition(v)
|
lineNumber := getItemPosition(v)
|
||||||
return state.Branches[lineNumber]
|
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
|
// may want to standardise how these select methods work
|
||||||
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
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 {
|
if len(state.Branches) == 0 {
|
||||||
return renderString(g, "main", "No branches for this repo")
|
return renderString(g, "main", "No branches for this repo")
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,18 @@ func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||||
}, nil)
|
}, 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 {
|
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)
|
commit, err := getSelectedCommit(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != ErrNoCommits {
|
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) {
|
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
panelWidth := 60
|
panelWidth := 60
|
||||||
// panelHeight := int(math.Ceil(float64(len(prompt)) / float64(panelWidth)))
|
|
||||||
panelHeight := getMessageHeight(prompt, panelWidth)
|
panelHeight := getMessageHeight(prompt, panelWidth)
|
||||||
return width/2 - panelWidth/2,
|
return width/2 - panelWidth/2,
|
||||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
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
|
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)
|
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 {
|
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||||
file, err := getSelectedFile(v)
|
file, err := getSelectedFile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -51,16 +51,20 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSelectedFile(v *gocui.View) (GitFile, error) {
|
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
|
||||||
if len(state.GitFiles) == 0 {
|
if len(state.GitFiles) == 0 {
|
||||||
return GitFile{}, ErrNoFiles
|
return GitFile{}, ErrNoFiles
|
||||||
}
|
}
|
||||||
lineNumber := getItemPosition(v)
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lineNumber := getItemPosition(filesView)
|
||||||
return state.GitFiles[lineNumber], nil
|
return state.GitFiles[lineNumber], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||||
file, err := getSelectedFile(v)
|
file, err := getSelectedFile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -79,7 +83,7 @@ func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIgnoreFile(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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -90,30 +94,52 @@ func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||||
return refreshFiles(g)
|
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 {
|
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"
|
gitFile, err := getSelectedFile(g)
|
||||||
item, err := getSelectedFile(v)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != ErrNoFiles {
|
if err != ErrNoFiles {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
renderString(g, "main", "No changed files")
|
renderString(g, "main", "No changed files")
|
||||||
colorLog(color.FgRed, "error")
|
colorLog(color.FgRed, "error")
|
||||||
return renderString(g, "options", baseString)
|
return renderfilesOptions(g, nil)
|
||||||
}
|
}
|
||||||
var optionsString string
|
renderfilesOptions(g, &gitFile)
|
||||||
if item.Tracked {
|
var content string
|
||||||
optionsString = baseString + ", d: checkout"
|
if gitFile.HasMergeConflicts {
|
||||||
} else {
|
return refreshMergePanel(g)
|
||||||
optionsString = baseString + ", d: delete"
|
|
||||||
}
|
}
|
||||||
renderString(g, "options", optionsString)
|
|
||||||
diff := getDiff(item)
|
content = getDiff(gitFile)
|
||||||
return renderString(g, "main", diff)
|
return renderString(g, "main", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
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")
|
return createErrorPanel(g, "There are no staged files to commit")
|
||||||
}
|
}
|
||||||
createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -146,31 +172,64 @@ func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
return genericFileOpen(g, v, sublimeOpenFile)
|
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 {
|
func refreshFiles(g *gocui.Gui) error {
|
||||||
filesView, err := g.View("files")
|
filesView, err := g.View("files")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
refreshStateGitFiles()
|
||||||
// get files to stage
|
|
||||||
gitFiles := getGitStatusFiles()
|
|
||||||
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
|
|
||||||
|
|
||||||
filesView.Clear()
|
filesView.Clear()
|
||||||
red := color.New(color.FgRed)
|
|
||||||
green := color.New(color.FgGreen)
|
|
||||||
for _, gitFile := range state.GitFiles {
|
for _, gitFile := range state.GitFiles {
|
||||||
if !gitFile.Tracked {
|
renderGitFile(gitFile, filesView)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
correctCursor(filesView)
|
correctCursor(filesView)
|
||||||
if filesView == g.CurrentView() {
|
if filesView == g.CurrentView() {
|
||||||
|
@ -181,7 +240,7 @@ func refreshFiles(g *gocui.Gui) error {
|
||||||
|
|
||||||
func pullFiles(g *gocui.Gui, v *gocui.View) error {
|
func pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
devLog("pulling...")
|
devLog("pulling...")
|
||||||
createSimpleConfirmationPanel(g, v, "", "Pulling...")
|
createMessagePanel(g, v, "", "Pulling...")
|
||||||
go func() {
|
go func() {
|
||||||
if output, err := gitPull(); err != nil {
|
if output, err := gitPull(); err != nil {
|
||||||
createErrorPanel(g, output)
|
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 {
|
func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
devLog("pushing...")
|
devLog("pushing...")
|
||||||
createSimpleConfirmationPanel(g, v, "", "Pushing...")
|
createMessagePanel(g, v, "", "Pushing...")
|
||||||
go func() {
|
go func() {
|
||||||
if output, err := gitPush(); err != nil {
|
if output, err := gitPush(); err != nil {
|
||||||
createErrorPanel(g, output)
|
createErrorPanel(g, output)
|
||||||
|
@ -211,3 +270,31 @@ func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
}()
|
}()
|
||||||
return nil
|
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
|
HasUnstagedChanges bool
|
||||||
Tracked bool
|
Tracked bool
|
||||||
Deleted bool
|
Deleted bool
|
||||||
|
HasMergeConflicts bool
|
||||||
DisplayString string
|
DisplayString string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +31,6 @@ type Branch struct {
|
||||||
Type string
|
Type string
|
||||||
BaseBranch string
|
BaseBranch string
|
||||||
DisplayString string
|
DisplayString string
|
||||||
DisplayColor color.Attribute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit : A git commit
|
// Commit : A git commit
|
||||||
|
@ -133,8 +133,8 @@ func branchPropertiesFromName(name string) (string, string, color.Attribute) {
|
||||||
return "other", name, color.FgWhite
|
return "other", name, color.FgWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
func coloredString(str string, colour color.Attribute) string {
|
func coloredString(str string, colour *color.Color) string {
|
||||||
return color.New(colour).SprintFunc()(fmt.Sprint(str))
|
return colour.SprintFunc()(fmt.Sprint(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
func withPadding(str string, padding int) string {
|
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 {
|
func branchFromLine(line string, index int) Branch {
|
||||||
recency, name := branchStringParts(line)
|
recency, name := branchStringParts(line)
|
||||||
branchType, branchBase, colour := branchPropertiesFromName(name)
|
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
recency = " *"
|
recency = " *"
|
||||||
}
|
}
|
||||||
|
colour := color.New(colourAttr)
|
||||||
displayString := withPadding(recency, 4) + coloredString(name, colour)
|
displayString := withPadding(recency, 4) + coloredString(name, colour)
|
||||||
return Branch{
|
return Branch{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: branchType,
|
Type: branchType,
|
||||||
BaseBranch: branchBase,
|
BaseBranch: branchBase,
|
||||||
DisplayString: displayString,
|
DisplayString: displayString,
|
||||||
DisplayColor: colour,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,10 +206,11 @@ func getGitStatusFiles() []GitFile {
|
||||||
gitFile := GitFile{
|
gitFile := GitFile{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
DisplayString: statusString,
|
DisplayString: statusString,
|
||||||
HasStagedChanges: tracked && stagedChange != " ",
|
HasStagedChanges: tracked && stagedChange != " " && stagedChange != "U",
|
||||||
HasUnstagedChanges: !tracked || unstagedChange != " ",
|
HasUnstagedChanges: !tracked || unstagedChange != " ",
|
||||||
Tracked: tracked,
|
Tracked: tracked,
|
||||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||||
|
HasMergeConflicts: statusString[0:2] == "UU",
|
||||||
}
|
}
|
||||||
gitFiles = append(gitFiles, gitFile)
|
gitFiles = append(gitFiles, gitFile)
|
||||||
}
|
}
|
||||||
|
@ -328,14 +329,15 @@ func getDiff(file GitFile) string {
|
||||||
trackedArg = "--no-index /dev/null "
|
trackedArg = "--no-index /dev/null "
|
||||||
}
|
}
|
||||||
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
|
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
|
||||||
s, err := runCommand(command)
|
// for now we assume an error means the file was deleted
|
||||||
if err != nil {
|
s, _ := runCommand(command)
|
||||||
// for now we assume an error means the file was deleted
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func catFile(file string) (string, error) {
|
||||||
|
return runDirectCommand("cat " + file)
|
||||||
|
}
|
||||||
|
|
||||||
func stageFile(file string) error {
|
func stageFile(file string) error {
|
||||||
_, err := runCommand("git add " + file)
|
_, err := runCommand("git add " + file)
|
||||||
return err
|
return err
|
||||||
|
@ -350,6 +352,14 @@ func getGitStatus() (string, error) {
|
||||||
return runCommand("git status --untracked-files=all --short")
|
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 {
|
func removeFile(file GitFile) error {
|
||||||
// if the file isn't tracked, we assume you want to delete it
|
// if the file isn't tracked, we assume you want to delete it
|
||||||
if !file.Tracked {
|
if !file.Tracked {
|
||||||
|
@ -398,6 +408,14 @@ func gitListStash() (string, error) {
|
||||||
return runDirectCommand("git stash list")
|
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) {
|
func gitUpstreamDifferenceCount() (string, string) {
|
||||||
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
123
gui.go
123
gui.go
|
@ -8,22 +8,38 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
// "strings"
|
// "strings"
|
||||||
|
"github.com/golang-collections/collections/stack"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stateType struct {
|
type stateType struct {
|
||||||
GitFiles []GitFile
|
GitFiles []GitFile
|
||||||
Branches []Branch
|
Branches []Branch
|
||||||
Commits []Commit
|
Commits []Commit
|
||||||
StashEntries []StashEntry
|
StashEntries []StashEntry
|
||||||
PreviousView string
|
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{
|
var state = stateType{
|
||||||
GitFiles: make([]GitFile, 0),
|
GitFiles: make([]GitFile, 0),
|
||||||
PreviousView: "files",
|
PreviousView: "files",
|
||||||
Commits: make([]Commit, 0),
|
Commits: make([]Commit, 0),
|
||||||
StashEntries: make([]StashEntry, 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 {
|
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
@ -44,6 +60,10 @@ func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return refreshSidePanels(g)
|
||||||
|
}
|
||||||
|
|
||||||
func keybindings(g *gocui.Gui) error {
|
func keybindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
|
if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextView); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -66,6 +86,15 @@ func keybindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil {
|
if err := g.SetKeybinding("", gocui.KeyPgdn, gocui.ModNone, scrollDownMain); err != nil {
|
||||||
return err
|
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 {
|
if err := g.SetKeybinding("files", 'c', gocui.ModNone, handleCommitPress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -75,24 +104,45 @@ func keybindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil {
|
if err := g.SetKeybinding("files", 'd', gocui.ModNone, handleFileRemove); err != nil {
|
||||||
return err
|
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 {
|
if err := g.SetKeybinding("files", 'o', gocui.ModNone, handleFileOpen); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil {
|
if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil {
|
||||||
return err
|
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 {
|
if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil {
|
if err := g.SetKeybinding("files", 'S', gocui.ModNone, handleStashSave); err != nil {
|
||||||
return err
|
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 {
|
if err := g.SetKeybinding("branches", gocui.KeySpace, gocui.ModNone, handleBranchPress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -102,6 +152,9 @@ func keybindings(g *gocui.Gui) error {
|
||||||
if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil {
|
if err := g.SetKeybinding("branches", 'n', gocui.ModNone, handleNewBranch); err != nil {
|
||||||
return err
|
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 {
|
if err := g.SetKeybinding("commits", 's', gocui.ModNone, handleCommitSquashDown); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -127,12 +180,14 @@ func keybindings(g *gocui.Gui) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func layout(g *gocui.Gui) error {
|
func layout(g *gocui.Gui) error {
|
||||||
|
g.Highlight = true
|
||||||
|
g.SelFgColor = gocui.AttrBold
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
leftSideWidth := width / 3
|
leftSideWidth := width / 3
|
||||||
statusFilesBoundary := 2
|
statusFilesBoundary := 2
|
||||||
filesBranchesBoundary := height - 20
|
filesBranchesBoundary := 2 * height / 5 // height - 20
|
||||||
commitsBranchesBoundary := height - 10
|
commitsBranchesBoundary := 3 * height / 5 // height - 10
|
||||||
commitsStashBoundary := height - 5
|
commitsStashBoundary := height - 5 // height - 5
|
||||||
|
|
||||||
optionsTop := height - 2
|
optionsTop := height - 2
|
||||||
// hiding options if there's not enough space
|
// hiding options if there's not enough space
|
||||||
|
@ -241,35 +296,3 @@ func run() {
|
||||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gocui.ErrQuit
|
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]
|
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 {
|
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() {
|
go func() {
|
||||||
stashEntry := getSelectedStashEntry(v)
|
stashEntry := getSelectedStashEntry(v)
|
||||||
if stashEntry == nil {
|
if stashEntry == nil {
|
||||||
|
|
|
@ -21,6 +21,13 @@ func refreshStatus(g *gocui.Gui) error {
|
||||||
pushables, pullables := gitUpstreamDifferenceCount()
|
pushables, pullables := gitUpstreamDifferenceCount()
|
||||||
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
||||||
branches := state.Branches
|
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 {
|
if len(branches) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,15 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
||||||
|
|
||||||
func refreshSidePanels(g *gocui.Gui, v *gocui.View) error {
|
func refreshSidePanels(g *gocui.Gui) error {
|
||||||
refreshBranches(g)
|
refreshBranches(g)
|
||||||
refreshFiles(g)
|
refreshFiles(g)
|
||||||
refreshCommits(g)
|
refreshCommits(g)
|
||||||
|
@ -54,6 +53,9 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||||
case "confirmation":
|
case "confirmation":
|
||||||
return nil
|
return nil
|
||||||
case "main":
|
case "main":
|
||||||
|
// TODO: pull this out into a 'view focused' function
|
||||||
|
refreshMergePanel(g)
|
||||||
|
v.Highlight = false
|
||||||
return nil
|
return nil
|
||||||
case "commits":
|
case "commits":
|
||||||
return handleCommitSelect(g, v)
|
return handleCommitSelect(g, v)
|
||||||
|
@ -72,6 +74,7 @@ func returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||||
return switchFocus(g, v, previousView)
|
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 {
|
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 assume we'll never want to return focus to a confirmation panel i.e.
|
||||||
// we should never stack confirmation panels
|
// 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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,24 +121,20 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
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 {
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
if v != nil {
|
// swallowing cursor movements in main
|
||||||
cx, cy := v.Cursor()
|
// TODO: pull this out
|
||||||
ox, oy := v.Origin()
|
if v == nil || v.Name() == "main" {
|
||||||
if cy+oy >= len(v.BufferLines())-2 {
|
return nil
|
||||||
return nil
|
}
|
||||||
}
|
cx, cy := v.Cursor()
|
||||||
if err := v.SetCursor(cx, cy+1); err != nil {
|
ox, oy := v.Origin()
|
||||||
if err := v.SetOrigin(ox, oy+1); err != nil {
|
if cy+oy >= len(v.BufferLines())-2 {
|
||||||
return err
|
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
|
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
|
// if the cursor down past the last item, move it up one
|
||||||
func correctCursor(v *gocui.View) error {
|
func correctCursor(v *gocui.View) error {
|
||||||
cx, cy := v.Cursor()
|
cx, cy := v.Cursor()
|
||||||
|
@ -154,8 +162,6 @@ func correctCursor(v *gocui.View) error {
|
||||||
|
|
||||||
func renderString(g *gocui.Gui, viewName, s string) error {
|
func renderString(g *gocui.Gui, viewName, s string) error {
|
||||||
g.Update(func(*gocui.Gui) error {
|
g.Update(func(*gocui.Gui) error {
|
||||||
timeStart := time.Now()
|
|
||||||
colorLog(color.FgRed, viewName)
|
|
||||||
v, err := g.View(viewName)
|
v, err := g.View(viewName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -163,7 +169,6 @@ func renderString(g *gocui.Gui, viewName, s string) error {
|
||||||
v.Clear()
|
v.Clear()
|
||||||
fmt.Fprint(v, s)
|
fmt.Fprint(v, s)
|
||||||
v.Wrap = true
|
v.Wrap = true
|
||||||
devLog("render time: ", time.Now().Sub(timeStart))
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
@ -179,3 +184,16 @@ func splitLines(multilineString string) []string {
|
||||||
}
|
}
|
||||||
return lines
|
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…
Add table
Add a link
Reference in a new issue