1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-15 11:56:37 +02:00

merge with develop

This commit is contained in:
Jesse Duffield 2018-07-21 16:06:38 +10:00
commit a47c889cbb
12 changed files with 1178 additions and 1044 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ commands.log
extra/lgit.rb extra/lgit.rb
notes/go.notes notes/go.notes
TODO.notes TODO.notes
TODO.md

View File

@ -1,17 +1,21 @@
package main package main
import ( import (
"fmt" "fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
) )
func handleBranchPress(g *gocui.Gui, v *gocui.View) error { func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
branch := getSelectedBranch(v) index := getItemPosition(v)
if output, err := gitCheckout(branch.Name, false); err != nil { if index == 0 {
createErrorPanel(g, output) return createErrorPanel(g, "You have already checked out this branch")
} }
return refreshSidePanels(g) branch := getSelectedBranch(v)
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
} }
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
@ -25,87 +29,87 @@ func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
} }
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(trimmedContent(v), false); err != nil { if output, err := gitCheckout(trimmedContent(v), false); err != nil {
return createErrorPanel(g, output) return createErrorPanel(g, output)
} }
return refreshSidePanels(g) return refreshSidePanels(g)
}) })
return nil return nil
} }
func handleNewBranch(g *gocui.Gui, v *gocui.View) error { func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := state.Branches[0] branch := state.Branches[0]
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(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) refreshSidePanels(g)
return handleBranchSelect(g, v) return handleBranchSelect(g, v)
}) })
return nil return nil
} }
func handleMerge(g *gocui.Gui, v *gocui.View) error { func handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0] checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v) selectedBranch := getSelectedBranch(v)
defer refreshSidePanels(g) defer refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name { if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot merge a branch into itself") return createErrorPanel(g, "You cannot merge a branch into itself")
} }
if output, err := gitMerge(selectedBranch.Name); err != nil { if output, err := gitMerge(selectedBranch.Name); err != nil {
return createErrorPanel(g, output) return createErrorPanel(g, output)
} }
return nil 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 { func renderBranchesOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{ return renderOptionsMap(g, map[string]string{
"space": "checkout", "space": "checkout",
"f": "force checkout", "f": "force checkout",
"m": "merge", "m": "merge",
"c": "checkout by name", "c": "checkout by name",
"n": "checkout new branch", "n": "new branch",
}) })
} }
// 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 {
if err := renderBranchesOptions(g); err != nil { if err := renderBranchesOptions(g); err != nil {
return err return err
} }
// This really shouldn't happen: there should always be a master branch // 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")
} }
go func() { go func() {
branch := getSelectedBranch(v) branch := getSelectedBranch(v)
diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) diff, _ := getBranchDiff(branch.Name, branch.BaseBranch)
renderString(g, "main", diff) renderString(g, "main", diff)
}() }()
return nil return nil
} }
// refreshStatus is called at the end of this because that's when we can // refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from // be sure there is a state.Branches array to pick the current branch from
func refreshBranches(g *gocui.Gui) error { func refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches") v, err := g.View("branches")
if err != nil { if err != nil {
panic(err) panic(err)
} }
state.Branches = getGitBranches() state.Branches = getGitBranches()
v.Clear() v.Clear()
for _, branch := range state.Branches { for _, branch := range state.Branches {
fmt.Fprintln(v, branch.DisplayString) fmt.Fprintln(v, branch.DisplayString)
} }
resetOrigin(v) resetOrigin(v)
return refreshStatus(g) return refreshStatus(g)
}) })
return nil return nil
} }

View File

@ -2,65 +2,65 @@ package main
import ( import (
// "io" // "io"
// "io/ioutil" // "io/ioutil"
// "strings" // "strings"
"errors" "errors"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
) )
var ( var (
// ErrNoFiles : when there are no modified files in the repo // ErrNoFiles : when there are no modified files in the repo
ErrNoFiles = errors.New("No changed files") ErrNoFiles = errors.New("No changed files")
) )
func stagedFiles(files []GitFile) []GitFile { func stagedFiles(files []GitFile) []GitFile {
result := make([]GitFile, 0) result := make([]GitFile, 0)
for _, file := range files { for _, file := range files {
if file.HasStagedChanges { if file.HasStagedChanges {
result = append(result, file) result = append(result, file)
} }
} }
return result return result
} }
func handleFilePress(g *gocui.Gui, v *gocui.View) error { func handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g) file, err := getSelectedFile(g)
if err != nil { if err != nil {
return err return err
} }
if file.HasUnstagedChanges { if file.HasUnstagedChanges {
stageFile(file.Name) stageFile(file.Name)
} else { } else {
unStageFile(file.Name) unStageFile(file.Name, file.Tracked)
} }
if err := refreshFiles(g); err != nil { if err := refreshFiles(g); err != nil {
return err return err
} }
if err := handleFileSelect(g, v); err != nil { if err := handleFileSelect(g, v); err != nil {
return err return err
} }
return nil return nil
} }
func getSelectedFile(g *gocui.Gui) (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
} }
filesView, err := g.View("files") filesView, err := g.View("files")
if err != nil { if err != nil {
panic(err) panic(err)
} }
lineNumber := getItemPosition(filesView) 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 {
@ -83,93 +83,96 @@ 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(g) file, err := getSelectedFile(g)
if err != nil { if err != nil {
return err return err
} }
if file.Tracked { if file.Tracked {
return createErrorPanel(g, "Cannot ignore tracked files") return createErrorPanel(g, "Cannot ignore tracked files")
} }
gitIgnore(file.Name) gitIgnore(file.Name)
return refreshFiles(g) return refreshFiles(g)
} }
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error { func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
optionsMap := map[string]string{ optionsMap := map[string]string{
"tab": "next panel", "tab": "next panel",
"S": "stash files", "S": "stash files",
"c": "commit changes", "c": "commit changes",
"o": "open", "o": "open",
"s": "open in sublime", "s": "open in sublime",
"i": "ignore", "v": "open in vscode",
"d": "delete", "i": "ignore",
"space": "toggle staged", "d": "delete",
} "space": "toggle staged",
if state.HasMergeConflicts { }
optionsMap["a"] = "abort merge" if state.HasMergeConflicts {
optionsMap["m"] = "resolve merge conflicts" optionsMap["a"] = "abort merge"
} optionsMap["m"] = "resolve merge conflicts"
if gitFile == nil { }
return renderOptionsMap(g, optionsMap) if gitFile == nil {
} return renderOptionsMap(g, optionsMap)
if gitFile.Tracked { }
optionsMap["d"] = "checkout" if gitFile.Tracked {
} optionsMap["d"] = "checkout"
return renderOptionsMap(g, optionsMap) }
return renderOptionsMap(g, optionsMap)
} }
func handleFileSelect(g *gocui.Gui, v *gocui.View) error { func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gitFile, err := getSelectedFile(g) gitFile, err := getSelectedFile(g)
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") return renderfilesOptions(g, nil)
return renderfilesOptions(g, nil) }
} renderfilesOptions(g, &gitFile)
renderfilesOptions(g, &gitFile) var content string
var content string if gitFile.HasMergeConflicts {
if gitFile.HasMergeConflicts { return refreshMergePanel(g)
return refreshMergePanel(g) }
}
content = getDiff(gitFile) content = getDiff(gitFile)
return renderString(g, "main", content) 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 && !state.HasMergeConflicts { 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 {
message := trimmedContent(v) message := trimmedContent(v)
if message == "" { if message == "" {
return createErrorPanel(g, "You cannot commit without a commit message") return createErrorPanel(g, "You cannot commit without a commit message")
} }
if err := gitCommit(message); err != nil { if err := gitCommit(message); err != nil {
panic(err) panic(err)
} }
refreshFiles(g) refreshFiles(g)
return refreshCommits(g) return refreshCommits(g)
}) })
return nil return nil
} }
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(g) file, err := getSelectedFile(g)
if err != nil { if err != nil {
return err return err
} }
_, err = open(file.Name) _, err = open(file.Name)
return err return err
} }
func handleFileOpen(g *gocui.Gui, v *gocui.View) error { func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, openFile) return genericFileOpen(g, v, openFile)
} }
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile) return genericFileOpen(g, v, sublimeOpenFile)
}
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile)
} }
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
@ -177,128 +180,129 @@ func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
} }
func refreshStateGitFiles() { func refreshStateGitFiles() {
// get files to stage // get files to stage
gitFiles := getGitStatusFiles() gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
updateHasMergeConflictStatus() updateHasMergeConflictStatus()
} }
func updateHasMergeConflictStatus() error { func updateHasMergeConflictStatus() error {
merging, err := isInMergeState() merging, err := isInMergeState()
if err != nil { if err != nil {
return err return err
} }
state.HasMergeConflicts = merging state.HasMergeConflicts = merging
return nil return nil
} }
func renderGitFile(gitFile GitFile, filesView *gocui.View) { func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// potentially inefficient to be instantiating these color // potentially inefficient to be instantiating these color
// objects with each render // objects with each render
red := color.New(color.FgRed) red := color.New(color.FgRed)
green := color.New(color.FgGreen) green := color.New(color.FgGreen)
if !gitFile.Tracked { if !gitFile.Tracked && !gitFile.HasStagedChanges {
red.Fprintln(filesView, gitFile.DisplayString) red.Fprintln(filesView, gitFile.DisplayString)
return return
} }
green.Fprint(filesView, gitFile.DisplayString[0:1]) green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3]) red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges { if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name) red.Fprintln(filesView, gitFile.Name)
} else { } else {
green.Fprintln(filesView, gitFile.Name) green.Fprintln(filesView, gitFile.Name)
} }
} }
func catSelectedFile(g *gocui.Gui) (string, error) { func catSelectedFile(g *gocui.Gui) (string, error) {
item, err := getSelectedFile(g) item, err := getSelectedFile(g)
if err != nil { if err != nil {
if err != ErrNoFiles { if err != ErrNoFiles {
return "", err return "", err
} }
return "", renderString(g, "main", "No file to display") return "", renderString(g, "main", "No file to display")
} }
cat, err := catFile(item.Name) cat, err := catFile(item.Name)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return cat, nil 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() refreshStateGitFiles()
filesView.Clear() filesView.Clear()
for _, gitFile := range state.GitFiles { for _, gitFile := range state.GitFiles {
renderGitFile(gitFile, filesView) renderGitFile(gitFile, filesView)
} }
correctCursor(filesView) correctCursor(filesView)
if filesView == g.CurrentView() { if filesView == g.CurrentView() {
handleFileSelect(g, filesView) handleFileSelect(g, filesView)
} }
return nil return nil
} }
func pullFiles(g *gocui.Gui, v *gocui.View) error { func pullFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pulling...") devLog("pulling...")
createMessagePanel(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)
} else { } else {
closeConfirmationPrompt(g) closeConfirmationPrompt(g)
refreshCommits(g) refreshCommits(g)
refreshFiles(g) refreshStatus(g)
refreshStatus(g) devLog("pulled.")
devLog("pulled.") }
} refreshFiles(g)
}() }()
return nil return nil
} }
func pushFiles(g *gocui.Gui, v *gocui.View) error { func pushFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pushing...") devLog("pushing...")
createMessagePanel(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)
} else { } else {
closeConfirmationPrompt(g) closeConfirmationPrompt(g)
refreshCommits(g) refreshCommits(g)
refreshStatus(g) refreshStatus(g)
devLog("pushed.") devLog("pushed.")
} }
}() }()
return nil return nil
} }
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main") mergeView, err := g.View("main")
if err != nil { if err != nil {
return err return err
} }
file, err := getSelectedFile(g) file, err := getSelectedFile(g)
if err != nil { if err != nil {
if err != ErrNoFiles { if err != ErrNoFiles {
return err return err
} }
return nil return nil
} }
if !file.HasMergeConflicts { if !file.HasMergeConflicts {
return nil return nil
} }
switchFocus(g, v, mergeView) switchFocus(g, v, mergeView)
return refreshMergePanel(g) return refreshMergePanel(g)
} }
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error { func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
output, err := gitAbortMerge() output, err := gitAbortMerge()
if err != nil { if err != nil {
return createErrorPanel(g, output) return createErrorPanel(g, output)
} }
createMessagePanel(g, v, "", "Merge aborted") createMessagePanel(g, v, "", "Merge aborted")
return refreshFiles(g) refreshStatus(g)
return refreshFiles(g)
} }

View File

@ -2,268 +2,317 @@ package main
import ( import (
// "log" // "log"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
) )
// GitFile : A staged/unstaged file // GitFile : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix // TODO: decide whether to give all of these the Git prefix
type GitFile struct { type GitFile struct {
Name string Name string
HasStagedChanges bool HasStagedChanges bool
HasUnstagedChanges bool HasUnstagedChanges bool
Tracked bool Tracked bool
Deleted bool Deleted bool
HasMergeConflicts bool HasMergeConflicts bool
DisplayString string DisplayString string
} }
// Branch : A git branch // Branch : A git branch
type Branch struct { type Branch struct {
Name string Name string
Type string Type string
BaseBranch string BaseBranch string
DisplayString string DisplayString string
} }
// Commit : A git commit // Commit : A git commit
type Commit struct { type Commit struct {
Sha string Sha string
Name string Name string
Pushed bool Pushed bool
DisplayString string DisplayString string
} }
// StashEntry : A git stash entry // StashEntry : A git stash entry
type StashEntry struct { type StashEntry struct {
Index int Index int
Name string Name string
DisplayString string DisplayString string
} }
// Map (from https://gobyexample.com/collection-functions) // Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string { func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs)) vsm := make([]string, len(vs))
for i, v := range vs { for i, v := range vs {
vsm[i] = f(v) vsm[i] = f(v)
} }
return vsm return vsm
} }
func includesString(list []string, a string) bool { func includesString(list []string, a string) bool {
for _, b := range list { for _, b := range list {
if b == a { if b == a {
return true return true
} }
} }
return false return false
} }
// not sure how to genericise this because []interface{} doesn't accept e.g. // not sure how to genericise this because []interface{} doesn't accept e.g.
// []int arguments // []int arguments
func includesInt(list []int, a int) bool { func includesInt(list []int, a int) bool {
for _, b := range list { for _, b := range list {
if b == a { if b == a {
return true return true
} }
} }
return false return false
} }
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
if len(oldGitFiles) == 0 { if len(oldGitFiles) == 0 {
return newGitFiles return newGitFiles
} }
appendedIndexes := make([]int, 0) appendedIndexes := make([]int, 0)
// retain position of files we already could see // retain position of files we already could see
result := make([]GitFile, 0) result := make([]GitFile, 0)
for _, oldGitFile := range oldGitFiles { for _, oldGitFile := range oldGitFiles {
for newIndex, newGitFile := range newGitFiles { for newIndex, newGitFile := range newGitFiles {
if oldGitFile.Name == newGitFile.Name { if oldGitFile.Name == newGitFile.Name {
result = append(result, newGitFile) result = append(result, newGitFile)
appendedIndexes = append(appendedIndexes, newIndex) appendedIndexes = append(appendedIndexes, newIndex)
break break
} }
} }
} }
// append any new files to the end // append any new files to the end
for index, newGitFile := range newGitFiles { for index, newGitFile := range newGitFiles {
if !includesInt(appendedIndexes, index) { if !includesInt(appendedIndexes, index) {
result = append(result, newGitFile) result = append(result, newGitFile)
} }
} }
return result return result
} }
func runDirectCommand(command string) (string, error) { func runDirectCommand(command string) (string, error) {
timeStart := time.Now() timeStart := time.Now()
commandLog(command) commandLog(command)
cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() cmdOut, err := exec.
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) Command("bash", "-c", command).
CombinedOutput()
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart))
return string(cmdOut), err return string(cmdOut), err
} }
func branchStringParts(branchString string) (string, string) { func branchStringParts(branchString string) (string, string) {
splitBranchName := strings.Split(branchString, "\t") // expect string to be something like '4w master`
return splitBranchName[0], splitBranchName[1] splitBranchName := strings.Split(branchString, "\t")
// if we have no \t then we have no recency, so just output that as blank
if len(splitBranchName) == 1 {
return "", branchString
}
return splitBranchName[0], splitBranchName[1]
} }
// branchPropertiesFromName : returns branch type, base, and color // branchPropertiesFromName : returns branch type, base, and color
func branchPropertiesFromName(name string) (string, string, color.Attribute) { func branchPropertiesFromName(name string) (string, string, color.Attribute) {
if strings.Contains(name, "feature/") { if strings.Contains(name, "feature/") {
return "feature", "develop", color.FgGreen return "feature", "develop", color.FgGreen
} else if strings.Contains(name, "bugfix/") { } else if strings.Contains(name, "bugfix/") {
return "bugfix", "develop", color.FgYellow return "bugfix", "develop", color.FgYellow
} else if strings.Contains(name, "hotfix/") { } else if strings.Contains(name, "hotfix/") {
return "hotfix", "master", color.FgRed return "hotfix", "master", color.FgRed
} }
return "other", name, color.FgWhite return "other", name, color.FgWhite
} }
func coloredString(str string, colour *color.Color) string { func coloredString(str string, colour *color.Color) string {
return 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 {
return str + strings.Repeat(" ", padding-len(str)) if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
} }
func branchFromLine(line string, index int) Branch { func branchFromLine(line string, index int) Branch {
recency, name := branchStringParts(line) recency, name := branchStringParts(line)
branchType, branchBase, colourAttr := branchPropertiesFromName(name) branchType, branchBase, colourAttr := branchPropertiesFromName(name)
if index == 0 { if index == 0 {
recency = " *" recency = " *"
} }
colour := color.New(colourAttr) 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,
} }
} }
func getGitBranches() []Branch { func getGitBranches() []Branch {
branches := make([]Branch, 0) branches := make([]Branch, 0)
// check if there are any branches // check if there are any branches
branchCheck, _ := runDirectCommand("git branch") branchCheck, _ := runDirectCommand("git branch")
if branchCheck == "" { if branchCheck == "" {
return branches return append(branches, branchFromLine("master", 0))
} }
rawString, _ := runDirectCommand(getBranchesCommand) rawString, _ := runDirectCommand(getBranchesCommand)
branchLines := splitLines(rawString) branchLines := splitLines(rawString)
if len(branchLines) == 0 { for i, line := range branchLines {
// sometimes the getBranchesCommand command returns nothing, in which case branches = append(branches, branchFromLine(line, i))
// we assume you've just init'd or cloned the repo and you've got master }
// checked out branches = getAndMergeFetchedBranches(branches)
branches = append(branches, branchFromLine(" *\tmaster", 0)) return branches
} }
for i, line := range branchLines {
branches = append(branches, branchFromLine(line, i)) func branchAlreadyStored(branchLine string, branches []Branch) bool {
} for _, branch := range branches {
return branches if branch.Name == branchLine {
return true
}
}
return false
}
// here branches contains all the branches that we've checked out, along with
// the recency. In this function we append the branches that are in our heads
// directory i.e. things we've fetched but haven't necessarily checked out.
// Worth mentioning this has nothing to do with the 'git merge' operation
func getAndMergeFetchedBranches(branches []Branch) []Branch {
rawString, _ := runDirectCommand(getHeadsCommand)
branchLines := splitLines(rawString)
for _, line := range branchLines {
if branchAlreadyStored(line, branches) {
continue
}
branches = append(branches, branchFromLine(line, len(branches)))
}
return branches
} }
// TODO: DRY up this function and getGitBranches // TODO: DRY up this function and getGitBranches
func getGitStashEntries() []StashEntry { func getGitStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0) stashEntries := make([]StashEntry, 0)
rawString, _ := runDirectCommand("git stash list --pretty='%gs'") rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
for i, line := range splitLines(rawString) { for i, line := range splitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i)) stashEntries = append(stashEntries, stashEntryFromLine(line, i))
} }
return stashEntries return stashEntries
} }
func stashEntryFromLine(line string, index int) StashEntry { func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{ return StashEntry{
Name: line, Name: line,
Index: index, Index: index,
DisplayString: line, DisplayString: line,
} }
} }
func getStashEntryDiff(index int) (string, error) { func getStashEntryDiff(index int) (string, error) {
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
} }
func getGitStatusFiles() []GitFile { func getGitStatusFiles() []GitFile {
statusOutput, _ := getGitStatus() statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput) statusStrings := splitLines(statusOutput)
gitFiles := make([]GitFile, 0) gitFiles := make([]GitFile, 0)
for _, statusString := range statusStrings { for _, statusString := range statusStrings {
stagedChange := statusString[0:1] change := statusString[0:2]
unstagedChange := statusString[1:2] stagedChange := change[0:1]
filename := statusString[3:] unstagedChange := statusString[1:2]
tracked := statusString[0:2] != "??" filename := statusString[3:]
gitFile := GitFile{ tracked := !includes([]string{"??", "A "}, change)
Name: filename, gitFile := GitFile{
DisplayString: statusString, Name: filename,
HasStagedChanges: tracked && stagedChange != " " && stagedChange != "U", DisplayString: statusString,
HasUnstagedChanges: !tracked || unstagedChange != " ", HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
Tracked: tracked, HasUnstagedChanges: unstagedChange != " ",
Deleted: unstagedChange == "D" || stagedChange == "D", Tracked: tracked,
HasMergeConflicts: statusString[0:2] == "UU", Deleted: unstagedChange == "D" || stagedChange == "D",
} HasMergeConflicts: change == "UU",
gitFiles = append(gitFiles, gitFile) }
} devLog("tracked", gitFile.Tracked)
return gitFiles devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges)
devLog("HasStagedChanges", gitFile.HasStagedChanges)
devLog("DisplayString", gitFile.DisplayString)
gitFiles = append(gitFiles, gitFile)
}
devLog(gitFiles)
return gitFiles
} }
func gitStashDo(index int, method string) (string, error) { func gitStashDo(index int, method string) (string, error) {
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}") return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
} }
func gitStashSave(message string) (string, error) { func gitStashSave(message string) (string, error) {
output, err := runCommand("git stash save \"" + message + "\"") output, err := runCommand("git stash save \"" + message + "\"")
if err != nil { if err != nil {
return output, err return output, err
} }
// if there are no local changes to save, the exit code is 0, but we want // if there are no local changes to save, the exit code is 0, but we want
// to raise an error // to raise an error
if output == "No local changes to save\n" { if output == "No local changes to save\n" {
return output, errors.New(output) return output, errors.New(output)
} }
return output, nil return output, nil
} }
func gitCheckout(branch string, force bool) (string, error) { func gitCheckout(branch string, force bool) (string, error) {
forceArg := "" forceArg := ""
if force { if force {
forceArg = "--force " forceArg = "--force "
} }
return runCommand("git checkout " + forceArg + branch) return runCommand("git checkout " + forceArg + branch)
} }
func runCommand(command string) (string, error) { func runCommand(command string) (string, error) {
commandStartTime := time.Now() commandStartTime := time.Now()
commandLog(command) commandLog(command)
splitCmd := strings.Split(command, " ") splitCmd := strings.Split(command, " ")
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
devLog("run command time: ", time.Now().Sub(commandStartTime)) devLog("run command time: ", time.Now().Sub(commandStartTime))
return string(cmdOut), err return string(cmdOut), err
} }
func openFile(filename string) (string, error) { func openFile(filename string) (string, error) {
return runCommand("open " + filename) return runCommand("open " + filename)
}
func vsCodeOpenFile(filename string) (string, error) {
return runCommand("code -r " + filename)
} }
func sublimeOpenFile(filename string) (string, error) { func sublimeOpenFile(filename string) (string, error) {
return runCommand("subl " + filename) return runCommand("subl " + filename)
} }
func getBranchDiff(branch string, baseBranch string) (string, error) { func getBranchDiff(branch string, baseBranch string) (string, error) {
@ -273,185 +322,192 @@ func getBranchDiff(branch string, baseBranch string) (string, error) {
} }
func verifyInGitRepo() { func verifyInGitRepo() {
if output, err := runCommand("git status"); err != nil { if output, err := runCommand("git status"); err != nil {
fmt.Println(output) fmt.Println(output)
os.Exit(1) os.Exit(1)
} }
} }
func getCommits() []Commit { func getCommits() []Commit {
pushables := gitCommitsToPush() pushables := gitCommitsToPush()
log := getLog() log := getLog()
commits := make([]Commit, 0) commits := make([]Commit, 0)
// now we can split it up and turn it into commits // now we can split it up and turn it into commits
lines := splitLines(log) lines := splitLines(log)
for _, line := range lines { for _, line := range lines {
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
sha := splitLine[0] sha := splitLine[0]
pushed := includesString(pushables, sha) pushed := includesString(pushables, sha)
commits = append(commits, Commit{ commits = append(commits, Commit{
Sha: sha, Sha: sha,
Name: strings.Join(splitLine[1:], " "), Name: strings.Join(splitLine[1:], " "),
Pushed: pushed, Pushed: pushed,
DisplayString: strings.Join(splitLine, " "), DisplayString: strings.Join(splitLine, " "),
}) })
} }
return commits return commits
} }
func getLog() string { func getLog() string {
// currently limiting to 30 for performance reasons // currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down // TODO: add lazyloading when you scroll down
result, err := runDirectCommand("git log --oneline -30") result, err := runDirectCommand("git log --oneline -30")
if err != nil { if err != nil {
// assume if there is an error there are no commits yet for this branch // assume if there is an error there are no commits yet for this branch
return "" return ""
} }
return result return result
} }
func gitIgnore(filename string) { func gitIgnore(filename string) {
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
panic(err) panic(err)
} }
} }
func gitShow(sha string) string { func gitShow(sha string) string {
result, err := runDirectCommand("git show --color " + sha) result, err := runDirectCommand("git show --color " + sha)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return result return result
} }
func getDiff(file GitFile) string { func getDiff(file GitFile) string {
cachedArg := "" cachedArg := ""
if file.HasStagedChanges { if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached " cachedArg = "--cached "
} }
deletedArg := "" deletedArg := ""
if file.Deleted { if file.Deleted {
deletedArg = "-- " deletedArg = "-- "
} }
trackedArg := "" trackedArg := ""
if !file.Tracked { if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null " trackedArg = "--no-index /dev/null "
} }
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name command := "git diff -b --color " + cachedArg + deletedArg + trackedArg + file.Name
// for now we assume an error means the file was deleted // for now we assume an error means the file was deleted
s, _ := runCommand(command) s, _ := runCommand(command)
return s return s
} }
func catFile(file string) (string, error) { func catFile(file string) (string, error) {
return runDirectCommand("cat " + file) 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
} }
func unStageFile(file string) error { func unStageFile(file string, tracked bool) error {
_, err := runCommand("git reset HEAD " + file) var command string
return err if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
devLog(command)
_, err := runCommand(command + file)
return err
} }
func getGitStatus() (string, error) { func getGitStatus() (string, error) {
return runCommand("git status --untracked-files=all --short") return runCommand("git status --untracked-files=all --short")
} }
func isInMergeState() (bool, error) { func isInMergeState() (bool, error) {
output, err := runCommand("git status --untracked-files=all") output, err := runCommand("git status --untracked-files=all")
if err != nil { if err != nil {
return false, err return false, err
} }
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil 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 {
_, err := runCommand("rm -rf ./" + file.Name) _, err := runCommand("rm -rf ./" + file.Name)
return err return err
} }
// if the file is tracked, we assume you want to just check it out // if the file is tracked, we assume you want to just check it out
_, err := runCommand("git checkout " + file.Name) _, err := runCommand("git checkout " + file.Name)
return err return err
} }
func gitCommit(message string) error { func gitCommit(message string) error {
_, err := runDirectCommand("git commit -m \"" + message + "\"") _, err := runDirectCommand("git commit -m \"" + message + "\"")
return err return err
} }
func gitPull() (string, error) { func gitPull() (string, error) {
return runDirectCommand("git pull --no-edit") return runDirectCommand("git pull --no-edit")
} }
func gitPush() (string, error) { func gitPush() (string, error) {
return runDirectCommand("git push -u") return runDirectCommand("git push -u")
} }
func gitSquashPreviousTwoCommits(message string) (string, error) { func gitSquashPreviousTwoCommits(message string) (string, error) {
return runDirectCommand("git reset --soft head^ && git commit --amend -m \"" + message + "\"") return runDirectCommand("git reset --soft head^ && git commit --amend -m \"" + message + "\"")
} }
func gitRenameCommit(message string) (string, error) { func gitRenameCommit(message string) (string, error) {
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"") return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
} }
func gitFetch() (string, error) { func gitFetch() (string, error) {
return runDirectCommand("git fetch") return runDirectCommand("git fetch")
} }
func gitResetToCommit(sha string) (string, error) { func gitResetToCommit(sha string) (string, error) {
return runDirectCommand("git reset " + sha) return runDirectCommand("git reset " + sha)
} }
func gitNewBranch(name string) (string, error) { func gitNewBranch(name string) (string, error) {
return runDirectCommand("git checkout -b " + name) return runDirectCommand("git checkout -b " + name)
} }
func gitListStash() (string, error) { func gitListStash() (string, error) {
return runDirectCommand("git stash list") return runDirectCommand("git stash list")
} }
func gitMerge(branchName string) (string, error) { func gitMerge(branchName string) (string, error) {
return runDirectCommand("git merge --no-edit " + branchName) return runDirectCommand("git merge --no-edit " + branchName)
} }
func gitAbortMerge() (string, error) { func gitAbortMerge() (string, error) {
return runDirectCommand("git merge --abort") 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 {
return "?", "?" return "?", "?"
} }
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count") pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
if err != nil { if err != nil {
return "?", "?" return "?", "?"
} }
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
} }
func gitCommitsToPush() []string { func gitCommitsToPush() []string {
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit") pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
if err != nil { if err != nil {
return make([]string, 0) return make([]string, 0)
} }
return splitLines(pushables) return splitLines(pushables)
} }
func gitCurrentBranchName() string { func gitCurrentBranchName() string {
branchName, err := runDirectCommand("git rev-parse --abbrev-ref HEAD") branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
// if there is an error, assume there are no branches yet // if there is an error, assume there are no branches yet
if err != nil { if err != nil {
return "" return ""
} }
return branchName return branchName
} }
const getBranchesCommand = `set -e const getBranchesCommand = `set -e
@ -467,6 +523,26 @@ git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | {
printf "%s\t%s\n" "$date" "$branch" printf "%s\t%s\n" "$date" "$branch"
fi fi
fi fi
done | sed 's/ days /d /g' | sed 's/ weeks /w /g' | sed 's/ hours /h /g' | sed 's/ minutes /m /g' | sed 's/ seconds /m /g' | sed 's/ago//g' | tr -d ' ' done \
| sed 's/ days /d /g' \
| sed 's/ day /d /g' \
| sed 's/ weeks /w /g' \
| sed 's/ week /w /g' \
| sed 's/ hours /h /g' \
| sed 's/ hour /h /g' \
| sed 's/ minutes /m /g' \
| sed 's/ minute /m /g' \
| sed 's/ seconds /s /g' \
| sed 's/ second /s /g' \
| sed 's/ago//g' \
| tr -d ' '
} }
` `
const getHeadsCommand = `git show-ref \
| grep 'refs/heads/\|refs/remotes/origin/' \
| sed 's/.*refs\/heads\///g' \
| sed 's/.*refs\/remotes\/origin\///g' \
| grep -v '^HEAD$' \
| sort \
| uniq`

263
gui.go
View File

@ -2,66 +2,67 @@ package main
import ( import (
// "io" // "io"
// "io/ioutil" // "io/ioutil"
"log" "log"
"time" "time"
// "strings"
"github.com/golang-collections/collections/stack" // "strings"
"github.com/jesseduffield/gocui" "github.com/golang-collections/collections/stack"
"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 HasMergeConflicts bool
ConflictIndex int ConflictIndex int
ConflictTop bool ConflictTop bool
Conflicts []conflict Conflicts []conflict
EditHistory *stack.Stack EditHistory *stack.Stack
} }
type conflict struct { type conflict struct {
start int start int
middle int middle int
end 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, ConflictIndex: 0,
ConflictTop: true, ConflictTop: true,
Conflicts: make([]conflict, 0), Conflicts: make([]conflict, 0),
EditHistory: stack.New(), EditHistory: stack.New(),
} }
func scrollUpMain(g *gocui.Gui, v *gocui.View) error { func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main") mainView, _ := g.View("main")
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
if oy >= 1 { if oy >= 1 {
return mainView.SetOrigin(ox, oy-1) return mainView.SetOrigin(ox, oy-1)
} }
return nil return nil
} }
func scrollDownMain(g *gocui.Gui, v *gocui.View) error { func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main") mainView, _ := g.View("main")
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) { if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+1) return mainView.SetOrigin(ox, oy+1)
} }
return nil return nil
} }
func handleRefresh(g *gocui.Gui, v *gocui.View) error { func handleRefresh(g *gocui.Gui, v *gocui.View) error {
return refreshSidePanels(g) return refreshSidePanels(g)
} }
func keybindings(g *gocui.Gui) error { func keybindings(g *gocui.Gui) error {
@ -186,119 +187,119 @@ func keybindings(g *gocui.Gui) error {
} }
func layout(g *gocui.Gui) error { func layout(g *gocui.Gui) error {
g.Highlight = true g.Highlight = true
g.SelFgColor = gocui.AttrBold g.SelFgColor = gocui.AttrBold
width, height := g.Size() width, height := g.Size()
leftSideWidth := width / 3 leftSideWidth := width / 3
statusFilesBoundary := 2 statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20 filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10 commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // 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
if height < 30 { if height < 30 {
optionsTop = height - 1 optionsTop = height - 1
} }
filesView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1) filesView, err := g.SetView("files", 0, statusFilesBoundary+1, leftSideWidth, filesBranchesBoundary-1)
if err != nil { if err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
filesView.Highlight = true filesView.Highlight = true
filesView.Title = "Files" filesView.Title = "Files"
} }
if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil { if v, err := g.SetView("status", 0, statusFilesBoundary-2, leftSideWidth, statusFilesBoundary); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.Title = "Status" v.Title = "Status"
} }
mainView, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop) mainView, err := g.SetView("main", leftSideWidth+1, 0, width-1, optionsTop)
if err != nil { if err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
mainView.Title = "Diff" mainView.Title = "Diff"
mainView.Wrap = true mainView.Wrap = true
} }
if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, commitsBranchesBoundary-1); err != nil { if v, err := g.SetView("branches", 0, filesBranchesBoundary, leftSideWidth, commitsBranchesBoundary-1); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.Title = "Branches" v.Title = "Branches"
} }
if v, err := g.SetView("commits", 0, commitsBranchesBoundary, leftSideWidth, commitsStashBoundary-1); err != nil { if v, err := g.SetView("commits", 0, commitsBranchesBoundary, leftSideWidth, commitsStashBoundary-1); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.Title = "Commits" v.Title = "Commits"
} }
if v, err := g.SetView("stash", 0, commitsStashBoundary, leftSideWidth, optionsTop); err != nil { if v, err := g.SetView("stash", 0, commitsStashBoundary, leftSideWidth, optionsTop); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.Title = "Stash" v.Title = "Stash"
} }
if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil { if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.BgColor = gocui.ColorBlue v.BgColor = gocui.ColorBlue
v.Frame = false v.Frame = false
v.Title = "Options" v.Title = "Options"
// these are only called once // these are only called once
handleFileSelect(g, filesView) handleFileSelect(g, filesView)
refreshFiles(g) refreshFiles(g)
refreshBranches(g) refreshBranches(g)
refreshCommits(g) refreshCommits(g)
refreshStashEntries(g) refreshStashEntries(g)
nextView(g, nil) nextView(g, nil)
} }
return nil return nil
} }
func fetch(g *gocui.Gui) { func fetch(g *gocui.Gui) {
gitFetch() gitFetch()
refreshStatus(g) refreshStatus(g)
} }
func run() { func run() {
g, err := gocui.NewGui(gocui.OutputNormal) g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
defer g.Close() defer g.Close()
// periodically fetching to check for upstream differences // periodically fetching to check for upstream differences
go func() { go func() {
for range time.Tick(time.Second * 60) { for range time.Tick(time.Second * 60) {
fetch(g) fetch(g)
} }
}() }()
g.SetManagerFunc(layout) g.SetManagerFunc(layout)
if err := keybindings(g); err != nil { if err := keybindings(g); err != nil {
log.Panicln(err) log.Panicln(err)
} }
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err) log.Panicln(err)
} }
} }
func quit(g *gocui.Gui, v *gocui.View) error { func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit return gocui.ErrQuit
} }

76
main.go
View File

@ -1,59 +1,69 @@
package main package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/user" "os/user"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
) )
var ( var (
startTime time.Time startTime time.Time
debugging bool debugging bool
) )
func homeDirectory() string { func homeDirectory() string {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return usr.HomeDir return usr.HomeDir
} }
func devLog(objects ...interface{}) { func devLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
} }
func colorLog(colour color.Attribute, objects ...interface{}) { func colorLog(colour color.Attribute, objects ...interface{}) {
localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...) localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
} }
func commandLog(objects ...interface{}) { func commandLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...) localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...)
} }
func localLog(colour color.Attribute, path string, objects ...interface{}) { func localLog(colour color.Attribute, path string, objects ...interface{}) {
if !debugging { if !debugging {
return return
} }
f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
defer f.Close() defer f.Close()
for _, object := range objects { for _, object := range objects {
colorFunction := color.New(colour).SprintFunc() colorFunction := color.New(colour).SprintFunc()
f.WriteString(colorFunction(fmt.Sprint(object)) + "\n") f.WriteString(colorFunction(fmt.Sprint(object)) + "\n")
} }
}
func navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
devLog("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
} }
func main() { func main() {
debuggingPointer := flag.Bool("debug", false, "a boolean") debuggingPointer := flag.Bool("debug", false, "a boolean")
flag.Parse() flag.Parse()
debugging = *debuggingPointer debugging = *debuggingPointer
devLog("\n\n\n\n\n\n\n\n\n\n") devLog("\n\n\n\n\n\n\n\n\n\n")
startTime = time.Now() startTime = time.Now()
verifyInGitRepo() verifyInGitRepo()
run() navigateToRepoRootDirectory()
run()
} }

View File

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

1
newFile Normal file
View File

@ -0,0 +1 @@
newFile

1
newFile2 Normal file
View File

@ -0,0 +1 @@
newFile

View File

@ -1,65 +1,65 @@
package main package main
import ( import (
"fmt" "fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
) )
func refreshStashEntries(g *gocui.Gui) error { func refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
v, err := g.View("stash") v, err := g.View("stash")
if err != nil { if err != nil {
panic(err) panic(err)
} }
state.StashEntries = getGitStashEntries() state.StashEntries = getGitStashEntries()
v.Clear() v.Clear()
for _, stashEntry := range state.StashEntries { for _, stashEntry := range state.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString) fmt.Fprintln(v, stashEntry.DisplayString)
} }
return resetOrigin(v) return resetOrigin(v)
}) })
return nil return nil
} }
func getSelectedStashEntry(v *gocui.View) *StashEntry { func getSelectedStashEntry(v *gocui.View) *StashEntry {
if len(state.StashEntries) == 0 { if len(state.StashEntries) == 0 {
return nil return nil
} }
lineNumber := getItemPosition(v) lineNumber := getItemPosition(v)
return &state.StashEntries[lineNumber] return &state.StashEntries[lineNumber]
} }
func renderStashOptions(g *gocui.Gui) error { func renderStashOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{ return renderOptionsMap(g, map[string]string{
"space": "apply", "space": "apply",
"k": "pop", "k": "pop",
"d": "drop", "d": "drop",
}) })
} }
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := renderStashOptions(g); err != nil { if err := renderStashOptions(g); err != nil {
return err return err
} }
go func() { go func() {
stashEntry := getSelectedStashEntry(v) stashEntry := getSelectedStashEntry(v)
if stashEntry == nil { if stashEntry == nil {
renderString(g, "main", "No stash entries") renderString(g, "main", "No stash entries")
return return
} }
diff, _ := getStashEntryDiff(stashEntry.Index) diff, _ := getStashEntryDiff(stashEntry.Index)
renderString(g, "main", diff) renderString(g, "main", diff)
}() }()
return nil return nil
} }
func handleStashApply(g *gocui.Gui, v *gocui.View) error { func handleStashApply(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "apply") return stashDo(g, v, "apply")
} }
func handleStashPop(g *gocui.Gui, v *gocui.View) error { func handleStashPop(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "pop") return stashDo(g, v, "pop")
} }
func handleStashDrop(g *gocui.Gui, v *gocui.View) error { func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
@ -70,24 +70,24 @@ func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
} }
func stashDo(g *gocui.Gui, v *gocui.View, method string) error { func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := getSelectedStashEntry(v) stashEntry := getSelectedStashEntry(v)
if stashEntry == nil { if stashEntry == nil {
return createErrorPanel(g, "No stash to "+method) return createErrorPanel(g, "No stash to "+method)
} }
if output, err := gitStashDo(stashEntry.Index, method); err != nil { if output, err := gitStashDo(stashEntry.Index, method); err != nil {
createErrorPanel(g, output) createErrorPanel(g, output)
} }
refreshStashEntries(g) refreshStashEntries(g)
return refreshFiles(g) return refreshFiles(g)
} }
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitStashSave(trimmedContent(v)); err != nil { if output, err := gitStashSave(trimmedContent(v)); err != nil {
createErrorPanel(g, output) createErrorPanel(g, output)
} }
refreshStashEntries(g) refreshStashEntries(g)
return refreshFiles(g) return refreshFiles(g)
}) })
return nil return nil
} }

37
test/generate_basic_repo.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash
# this script will make a repo with a master and develop branch, where we end up
# on the master branch and if we try and merge master we get a merge conflict
# call this command from the test directory:
# ./generate_basic_repo.sh; cd testrepo; gg; cd ..
# -e means exit if something fails
# -x means print out simple commands before running them
set -ex
reponame="testrepo"
rm -rf ${reponame}
mkdir ${reponame}
cd ${reponame}
git init
echo "Here is a story that has been told throuhg the ages" >> file1
git add file1
git commit -m "first commit"
git checkout -b develop
echo "once upon a time there was a dog" >> file1
git add file1
git commit -m "first commit on develop"
git checkout master
echo "once upon a time there was a cat" >> file1
git add file1
git commit -m "first commit on develop"
git merge develop # should have a merge conflict here

View File

@ -1,199 +1,198 @@
package main package main
import ( import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"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) error { func refreshSidePanels(g *gocui.Gui) error {
refreshBranches(g) refreshBranches(g)
refreshFiles(g) refreshFiles(g)
refreshCommits(g) refreshCommits(g)
return nil return nil
} }
func nextView(g *gocui.Gui, v *gocui.View) error { func nextView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string var focusedViewName string
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
focusedViewName = cyclableViews[0] focusedViewName = cyclableViews[0]
} else { } else {
for i := range cyclableViews { for i := range cyclableViews {
if v.Name() == cyclableViews[i] { if v.Name() == cyclableViews[i] {
focusedViewName = cyclableViews[i+1] focusedViewName = cyclableViews[i+1]
break break
} }
if i == len(cyclableViews)-1 { if i == len(cyclableViews)-1 {
devLog(v.Name() + " is not in the list of views") devLog(v.Name() + " is not in the list of views")
return nil return nil
} }
} }
} }
focusedView, err := g.View(focusedViewName) focusedView, err := g.View(focusedViewName)
if err != nil { if err != nil {
panic(err) panic(err)
return err }
} return switchFocus(g, v, focusedView)
return switchFocus(g, v, focusedView)
} }
func newLineFocused(g *gocui.Gui, v *gocui.View) error { func newLineFocused(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main") mainView, _ := g.View("main")
mainView.SetOrigin(0, 0) mainView.SetOrigin(0, 0)
switch v.Name() { switch v.Name() {
case "files": case "files":
return handleFileSelect(g, v) return handleFileSelect(g, v)
case "branches": case "branches":
return handleBranchSelect(g, v) return handleBranchSelect(g, v)
case "confirmation": case "confirmation":
return nil return nil
case "main": case "main":
// TODO: pull this out into a 'view focused' function // TODO: pull this out into a 'view focused' function
refreshMergePanel(g) refreshMergePanel(g)
v.Highlight = false v.Highlight = false
return nil return nil
case "commits": case "commits":
return handleCommitSelect(g, v) return handleCommitSelect(g, v)
case "stash": case "stash":
return handleStashEntrySelect(g, v) return handleStashEntrySelect(g, v)
default: default:
panic("No view matching newLineFocused switch statement") panic("No view matching newLineFocused switch statement")
} }
} }
func returnFocus(g *gocui.Gui, v *gocui.View) error { func returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(state.PreviousView) previousView, err := g.View(state.PreviousView)
if err != nil { if err != nil {
panic(err) panic(err)
} }
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 // 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
if oldView != nil && oldView.Name() != "confirmation" { if oldView != nil && oldView.Name() != "confirmation" {
oldView.Highlight = false oldView.Highlight = false
devLog("setting previous view to:", oldView.Name()) devLog("setting previous view to:", oldView.Name())
state.PreviousView = oldView.Name() state.PreviousView = oldView.Name()
} }
newView.Highlight = true newView.Highlight = true
devLog(newView.Name()) devLog(newView.Name())
if _, err := g.SetCurrentView(newView.Name()); err != nil { if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err return err
} }
g.Cursor = newView.Editable g.Cursor = newView.Editable
return newLineFocused(g, newView) return newLineFocused(g, newView)
} }
func getItemPosition(v *gocui.View) int { func getItemPosition(v *gocui.View) int {
_, cy := v.Cursor() _, cy := v.Cursor()
_, oy := v.Origin() _, oy := v.Origin()
return oy + cy return oy + cy
} }
func trimmedContent(v *gocui.View) string { func trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer()) return strings.TrimSpace(v.Buffer())
} }
func cursorUp(g *gocui.Gui, v *gocui.View) error { func cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out // TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil return nil
} }
ox, oy := v.Origin() ox, oy := v.Origin()
cx, cy := v.Cursor() cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil { if err := v.SetOrigin(ox, oy-1); err != nil {
return err return err
} }
} }
newLineFocused(g, v) newLineFocused(g, v)
return nil return nil
} }
func cursorDown(g *gocui.Gui, v *gocui.View) error { func cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out // TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil return nil
} }
cx, cy := v.Cursor() cx, cy := v.Cursor()
ox, oy := v.Origin() ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 { if cy+oy >= len(v.BufferLines())-2 {
return nil return nil
} }
if err := v.SetCursor(cx, cy+1); err != nil { if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil { if err := v.SetOrigin(ox, oy+1); err != nil {
return err return err
} }
} }
newLineFocused(g, v) newLineFocused(g, v)
return nil return nil
} }
func resetOrigin(v *gocui.View) error { func resetOrigin(v *gocui.View) error {
if err := v.SetCursor(0, 0); err != nil { if err := v.SetCursor(0, 0); err != nil {
return err return err
} }
return v.SetOrigin(0, 0) 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()
_, oy := v.Origin() _, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2 lineCount := len(v.BufferLines()) - 2
if cy >= lineCount-oy { if cy >= lineCount-oy {
return v.SetCursor(cx, lineCount-oy) return v.SetCursor(cx, lineCount-oy)
} }
return nil return nil
} }
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 {
v, err := g.View(viewName) v, err := g.View(viewName)
if err != nil { if err != nil {
panic(err) panic(err)
} }
v.Clear() v.Clear()
fmt.Fprint(v, s) fmt.Fprint(v, s)
v.Wrap = true v.Wrap = true
return nil return nil
}) })
return nil return nil
} }
func splitLines(multilineString string) []string { func splitLines(multilineString string) []string {
if multilineString == "" || multilineString == "\n" { if multilineString == "" || multilineString == "\n" {
return make([]string, 0) return make([]string, 0)
} }
lines := strings.Split(multilineString, "\n") lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" { if lines[len(lines)-1] == "" {
return lines[:len(lines)-1] return lines[:len(lines)-1]
} }
return lines return lines
} }
func optionsMapToString(optionsMap map[string]string) string { func optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0) optionsArray := make([]string, 0)
for key, description := range optionsMap { for key, description := range optionsMap {
optionsArray = append(optionsArray, key+": "+description) optionsArray = append(optionsArray, key+": "+description)
} }
sort.Strings(optionsArray) sort.Strings(optionsArray)
return strings.Join(optionsArray, ", ") return strings.Join(optionsArray, ", ")
} }
func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return renderString(g, "options", optionsMapToString(optionsMap)) return renderString(g, "options", optionsMapToString(optionsMap))
} }