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:
commit
a47c889cbb
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ commands.log
|
|||||||
extra/lgit.rb
|
extra/lgit.rb
|
||||||
notes/go.notes
|
notes/go.notes
|
||||||
TODO.notes
|
TODO.notes
|
||||||
|
TODO.md
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
424
files_panel.go
424
files_panel.go
@ -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)
|
||||||
}
|
}
|
||||||
|
626
gitcommands.go
626
gitcommands.go
@ -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
263
gui.go
@ -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
76
main.go
@ -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()
|
||||||
}
|
}
|
||||||
|
256
merge_panel.go
256
merge_panel.go
@ -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)
|
||||||
}
|
}
|
||||||
|
114
stash_panel.go
114
stash_panel.go
@ -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
37
test/generate_basic_repo.sh
Executable 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
|
281
view_helpers.go
281
view_helpers.go
@ -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))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user