mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-17 00:18:05 +02:00
tried to update to latest master
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,5 +4,4 @@ extra/lgit.rb
|
|||||||
notes/go.notes
|
notes/go.notes
|
||||||
TODO.notes
|
TODO.notes
|
||||||
TODO.md
|
TODO.md
|
||||||
test/testrepo/
|
test/repos/repo
|
||||||
test/repos/repo
|
|
||||||
|
25
Gopkg.lock
generated
25
Gopkg.lock
generated
@ -1,6 +1,14 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
|
||||||
|
name = "github.com/Sirupsen/logrus"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||||
|
version = "v1.0.6"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||||
name = "github.com/davecgh/go-spew"
|
name = "github.com/davecgh/go-spew"
|
||||||
@ -50,11 +58,11 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:e9b2b07a20f19d886267876b72ba15f2cbdeeeadd18030a4ce174b864e97c39e"
|
digest = "1:c9a848b0484a72da2dae28957b4f67501fe27fa38bc73f4713e454353c0a4a60"
|
||||||
name = "github.com/jesseduffield/gocui"
|
name = "github.com/jesseduffield/gocui"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
pruneopts = "NUT"
|
pruneopts = "NUT"
|
||||||
revision = "8cecad864fb0b099a5f55bf1c97fbc1daca103e0"
|
revision = "432b7f6215f81ef1aaa1b2d9b69887822923cf79"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
|
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
|
||||||
@ -88,6 +96,14 @@
|
|||||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||||
version = "v0.0.2"
|
version = "v0.0.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
|
||||||
|
name = "github.com/mgutz/str"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
|
||||||
|
version = "v1.2.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
|
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
|
||||||
@ -151,7 +167,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:c76f8b24a4d9b99b502fb7b61ad769125075cb570efff9b9b73e6c428629532d"
|
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
packages = [
|
packages = [
|
||||||
"cast5",
|
"cast5",
|
||||||
@ -170,6 +186,7 @@
|
|||||||
"ssh",
|
"ssh",
|
||||||
"ssh/agent",
|
"ssh/agent",
|
||||||
"ssh/knownhosts",
|
"ssh/knownhosts",
|
||||||
|
"ssh/terminal",
|
||||||
]
|
]
|
||||||
pruneopts = "NUT"
|
pruneopts = "NUT"
|
||||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||||
@ -282,10 +299,12 @@
|
|||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
input-imports = [
|
input-imports = [
|
||||||
|
"github.com/Sirupsen/logrus",
|
||||||
"github.com/davecgh/go-spew/spew",
|
"github.com/davecgh/go-spew/spew",
|
||||||
"github.com/fatih/color",
|
"github.com/fatih/color",
|
||||||
"github.com/golang-collections/collections/stack",
|
"github.com/golang-collections/collections/stack",
|
||||||
"github.com/jesseduffield/gocui",
|
"github.com/jesseduffield/gocui",
|
||||||
|
"github.com/mgutz/str",
|
||||||
"github.com/tcnksm/go-gitconfig",
|
"github.com/tcnksm/go-gitconfig",
|
||||||
"gopkg.in/src-d/go-git.v4",
|
"gopkg.in/src-d/go-git.v4",
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing",
|
"gopkg.in/src-d/go-git.v4/plumbing",
|
||||||
|
@ -39,4 +39,4 @@
|
|||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "gopkg.in/src-d/go-git.v4"
|
name = "gopkg.in/src-d/go-git.v4"
|
||||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
index := getItemPosition(v)
|
|
||||||
if index == 0 {
|
|
||||||
return createErrorPanel(g, "You have already checked out this branch")
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
branch := getSelectedBranch(v)
|
|
||||||
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitCheckout(branch.Name, true); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCheckoutByName(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 {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
branch := state.Branches[0]
|
|
||||||
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 {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshSidePanels(g)
|
|
||||||
return handleBranchSelect(g, v)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
checkedOutBranch := state.Branches[0]
|
|
||||||
selectedBranch := getSelectedBranch(v)
|
|
||||||
if checkedOutBranch.Name == selectedBranch.Name {
|
|
||||||
return createErrorPanel(g, "You cannot delete the checked out branch!")
|
|
||||||
}
|
|
||||||
return createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitDeleteBranch(selectedBranch.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
checkedOutBranch := state.Branches[0]
|
|
||||||
selectedBranch := getSelectedBranch(v)
|
|
||||||
defer refreshSidePanels(g)
|
|
||||||
if checkedOutBranch.Name == selectedBranch.Name {
|
|
||||||
return createErrorPanel(g, "You cannot merge a branch into itself")
|
|
||||||
}
|
|
||||||
if output, err := gitMerge(selectedBranch.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedBranch(v *gocui.View) Branch {
|
|
||||||
lineNumber := getItemPosition(v)
|
|
||||||
return state.Branches[lineNumber]
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderBranchesOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"space": "checkout",
|
|
||||||
"f": "force checkout",
|
|
||||||
"m": "merge",
|
|
||||||
"c": "checkout by name",
|
|
||||||
"n": "new branch",
|
|
||||||
"d": "delete branch",
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// may want to standardise how these select methods work
|
|
||||||
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := renderBranchesOptions(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// This really shouldn't happen: there should always be a master branch
|
|
||||||
if len(state.Branches) == 0 {
|
|
||||||
return renderString(g, "main", "No branches for this repo")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
branch := getSelectedBranch(v)
|
|
||||||
diff, err := getBranchGraph(branch.Name)
|
|
||||||
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
|
||||||
diff = "There is no tracking for this branch"
|
|
||||||
}
|
|
||||||
renderString(g, "main", diff)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
func refreshBranches(g *gocui.Gui) error {
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
v, err := g.View("branches")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
state.Branches = getGitBranches()
|
|
||||||
v.Clear()
|
|
||||||
for _, branch := range state.Branches {
|
|
||||||
fmt.Fprintln(v, branch.getDisplayString())
|
|
||||||
}
|
|
||||||
resetOrigin(v)
|
|
||||||
return refreshStatus(g)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "github.com/jesseduffield/gocui"
|
|
||||||
|
|
||||||
func handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
message := trimmedContent(v)
|
|
||||||
if message == "" {
|
|
||||||
return createErrorPanel(g, "You cannot commit without a commit message")
|
|
||||||
}
|
|
||||||
if output, err := gitCommit(g, message); err != nil {
|
|
||||||
if err == errNoUsername {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshFiles(g)
|
|
||||||
v.Clear()
|
|
||||||
v.SetCursor(0, 0)
|
|
||||||
g.SetViewOnBottom("commitMessage")
|
|
||||||
switchFocus(g, v, getFilesView(g))
|
|
||||||
return refreshCommits(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
g.SetViewOnBottom("commitMessage")
|
|
||||||
return switchFocus(g, v, getFilesView(g))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
// resising ahead of time so that the top line doesn't get hidden to make
|
|
||||||
// room for the cursor on the second line
|
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
|
||||||
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
|
|
||||||
if err != gocui.ErrUnknownView {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.EditNewLine()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return renderString(g, "options", "esc: close, enter: confirm")
|
|
||||||
}
|
|
176
commits_panel.go
176
commits_panel.go
@ -1,176 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNoCommits : When no commits are found for the branch
|
|
||||||
ErrNoCommits = errors.New("No commits for this branch")
|
|
||||||
)
|
|
||||||
|
|
||||||
func refreshCommits(g *gocui.Gui) error {
|
|
||||||
g.Update(func(*gocui.Gui) error {
|
|
||||||
state.Commits = getCommits()
|
|
||||||
v, err := g.View("commits")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
v.Clear()
|
|
||||||
red := color.New(color.FgRed)
|
|
||||||
yellow := color.New(color.FgYellow)
|
|
||||||
white := color.New(color.FgWhite)
|
|
||||||
shaColor := white
|
|
||||||
for _, commit := range state.Commits {
|
|
||||||
if commit.Pushed {
|
|
||||||
shaColor = red
|
|
||||||
} else {
|
|
||||||
shaColor = yellow
|
|
||||||
}
|
|
||||||
shaColor.Fprint(v, commit.Sha+" ")
|
|
||||||
white.Fprintln(v, commit.Name)
|
|
||||||
}
|
|
||||||
refreshStatus(g)
|
|
||||||
if g.CurrentView().Name() == "commits" {
|
|
||||||
handleCommitSelect(g, v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
|
||||||
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if output, err := gitResetToCommit(commit.Sha); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := refreshFiles(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
resetOrigin(commitView)
|
|
||||||
return handleCommitSelect(g, nil)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderCommitsOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"s": "squash down",
|
|
||||||
"r": "rename",
|
|
||||||
"g": "reset to this commit",
|
|
||||||
"f": "fixup commit",
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := renderCommitsOptions(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != ErrNoCommits {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return renderString(g, "main", "No commits for this branch")
|
|
||||||
}
|
|
||||||
commitText := gitShow(commit.Sha)
|
|
||||||
return renderString(g, "main", commitText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if getItemPosition(v) != 0 {
|
|
||||||
return createErrorPanel(g, "Can only squash topmost commit")
|
|
||||||
}
|
|
||||||
if len(state.Commits) == 1 {
|
|
||||||
return createErrorPanel(g, "You have no commits to squash with")
|
|
||||||
}
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
refreshStatus(g)
|
|
||||||
return handleCommitSelect(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to files panel
|
|
||||||
func anyUnStagedChanges(files []GitFile) bool {
|
|
||||||
for _, file := range files {
|
|
||||||
if file.Tracked && file.HasUnstagedChanges {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if len(state.Commits) == 1 {
|
|
||||||
return createErrorPanel(g, "You have no commits to squash with")
|
|
||||||
}
|
|
||||||
objectLog(state.GitFiles)
|
|
||||||
if anyUnStagedChanges(state.GitFiles) {
|
|
||||||
return createErrorPanel(g, "Can't fixup while there are unstaged changes")
|
|
||||||
}
|
|
||||||
branch := state.Branches[0]
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitSquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return refreshStatus(g)
|
|
||||||
}, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if getItemPosition(v) != 0 {
|
|
||||||
return createErrorPanel(g, "Can only rename topmost commit")
|
|
||||||
}
|
|
||||||
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitRenameCommit(v.Buffer()); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return handleCommitSelect(g, v)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
|
|
||||||
v, err := g.View("commits")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(state.Commits) == 0 {
|
|
||||||
return Commit{}, ErrNoCommits
|
|
||||||
}
|
|
||||||
lineNumber := getItemPosition(v)
|
|
||||||
if lineNumber > len(state.Commits)-1 {
|
|
||||||
devLog("potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
|
|
||||||
return state.Commits[len(state.Commits)-1], nil
|
|
||||||
}
|
|
||||||
return state.Commits[lineNumber], nil
|
|
||||||
}
|
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
## Global:
|
## Global:
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
|
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
|
||||||
<kbd>PgUp</kbd>/<kbd>PgDn</kbd>: scroll diff panel (use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
|
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
|
||||||
<kbd>q</kbd>: quit
|
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
|
||||||
<kbd>p</kbd>: pull
|
<kbd>q</kbd>: quit
|
||||||
<kbd>shift</kbd>+<kbd>P</kbd>: push
|
<kbd>p</kbd>: pull
|
||||||
|
<kbd>shift</kbd>+<kbd>P</kbd>: push
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Files Panel:
|
## Files Panel:
|
||||||
|
362
files_panel.go
362
files_panel.go
@ -1,362 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
|
|
||||||
// "io"
|
|
||||||
// "io/ioutil"
|
|
||||||
|
|
||||||
// "strings"
|
|
||||||
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNoFiles = errors.New("No changed files")
|
|
||||||
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func stagedFiles(files []GitFile) []GitFile {
|
|
||||||
result := make([]GitFile, 0)
|
|
||||||
for _, file := range files {
|
|
||||||
if file.HasStagedChanges {
|
|
||||||
result = append(result, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func stageSelectedFile(g *gocui.Gui) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return stageFile(file.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.HasMergeConflicts {
|
|
||||||
return handleSwitchToMerge(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.HasUnstagedChanges {
|
|
||||||
stageFile(file.Name)
|
|
||||||
} else {
|
|
||||||
unStageFile(file.Name, file.Tracked)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := refreshFiles(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleFileSelect(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !file.HasUnstagedChanges {
|
|
||||||
return createErrorPanel(g, "File has no unstaged changes to add")
|
|
||||||
}
|
|
||||||
if !file.Tracked {
|
|
||||||
return createErrorPanel(g, "Cannot git add --patch untracked files")
|
|
||||||
}
|
|
||||||
gitAddPatch(g, file.Name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
|
|
||||||
if len(state.GitFiles) == 0 {
|
|
||||||
return GitFile{}, errNoFiles
|
|
||||||
}
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
lineNumber := getItemPosition(filesView)
|
|
||||||
return state.GitFiles[lineNumber], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var deleteVerb string
|
|
||||||
if file.Tracked {
|
|
||||||
deleteVerb = "checkout"
|
|
||||||
} else {
|
|
||||||
deleteVerb = "delete"
|
|
||||||
}
|
|
||||||
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := removeFile(file); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return refreshFiles(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
if file.Tracked {
|
|
||||||
return createErrorPanel(g, "Cannot ignore tracked files")
|
|
||||||
}
|
|
||||||
gitIgnore(file.Name)
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
|
|
||||||
optionsMap := map[string]string{
|
|
||||||
"← → ↑ ↓": ShortLocalize("navigate", "navigate"),
|
|
||||||
"S": ShortLocalize("stashFiles", "stash files"),
|
|
||||||
"c": ShortLocalize("commitChanges", "commit changes"),
|
|
||||||
"o": ShortLocalize("open", "open"),
|
|
||||||
"i": ShortLocalize("ignore", "ignore"),
|
|
||||||
"d": ShortLocalize("delete", "delete"),
|
|
||||||
"space": ShortLocalize("toggleStaged", "toggle staged"),
|
|
||||||
"R": ShortLocalize("refresh", "refresh"),
|
|
||||||
"t": ShortLocalize("addPatch", "add patch"),
|
|
||||||
"e": ShortLocalize("edit", "edit"),
|
|
||||||
"PgUp/PgDn": ShortLocalize("scroll", "scroll"),
|
|
||||||
}
|
|
||||||
if state.HasMergeConflicts {
|
|
||||||
optionsMap["a"] = ShortLocalize("abortMerge", "abort merge")
|
|
||||||
optionsMap["m"] = ShortLocalize("resolveMergeConflicts", "resolve merge conflicts")
|
|
||||||
}
|
|
||||||
if gitFile == nil {
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
if gitFile.Tracked {
|
|
||||||
optionsMap["d"] = "checkout"
|
|
||||||
}
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
gitFile, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
renderString(g, "main", "No changed files")
|
|
||||||
return renderfilesOptions(g, nil)
|
|
||||||
}
|
|
||||||
renderfilesOptions(g, &gitFile)
|
|
||||||
var content string
|
|
||||||
if gitFile.HasMergeConflicts {
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
content = getDiff(gitFile)
|
|
||||||
return renderString(g, "main", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
|
||||||
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "There are no staged files to commit")
|
|
||||||
}
|
|
||||||
commitMessageView := getCommitMessageView(g)
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
g.SetViewOnTop("commitMessage")
|
|
||||||
switchFocus(g, filesView, commitMessageView)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
|
||||||
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "There are no staged files to commit")
|
|
||||||
}
|
|
||||||
runSubProcess(g, "git", "commit")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := open(g, file.Name); err != nil {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, editFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, openFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
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 {
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshStateGitFiles() {
|
|
||||||
// get files to stage
|
|
||||||
gitFiles := getGitStatusFiles()
|
|
||||||
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
|
|
||||||
updateHasMergeConflictStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateHasMergeConflictStatus() error {
|
|
||||||
merging, err := isInMergeState()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
state.HasMergeConflicts = merging
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
|
|
||||||
// potentially inefficient to be instantiating these color
|
|
||||||
// objects with each render
|
|
||||||
red := color.New(color.FgRed)
|
|
||||||
green := color.New(color.FgGreen)
|
|
||||||
if !gitFile.Tracked && !gitFile.HasStagedChanges {
|
|
||||||
red.Fprintln(filesView, gitFile.DisplayString)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
green.Fprint(filesView, gitFile.DisplayString[0:1])
|
|
||||||
red.Fprint(filesView, gitFile.DisplayString[1:3])
|
|
||||||
if gitFile.HasUnstagedChanges {
|
|
||||||
red.Fprintln(filesView, gitFile.Name)
|
|
||||||
} else {
|
|
||||||
green.Fprintln(filesView, gitFile.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func catSelectedFile(g *gocui.Gui) (string, error) {
|
|
||||||
item, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "", renderString(g, "main", "No file to display")
|
|
||||||
}
|
|
||||||
cat, err := catFile(item.Name)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return cat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshFiles(g *gocui.Gui) error {
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
refreshStateGitFiles()
|
|
||||||
filesView.Clear()
|
|
||||||
for _, gitFile := range state.GitFiles {
|
|
||||||
renderGitFile(gitFile, filesView)
|
|
||||||
}
|
|
||||||
correctCursor(filesView)
|
|
||||||
if filesView == g.CurrentView() {
|
|
||||||
handleFileSelect(g, filesView)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pullFiles(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
createMessagePanel(g, v, "", "Pulling...")
|
|
||||||
go func() {
|
|
||||||
if output, err := gitPull(); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
} else {
|
|
||||||
closeConfirmationPrompt(g)
|
|
||||||
refreshCommits(g)
|
|
||||||
refreshStatus(g)
|
|
||||||
}
|
|
||||||
refreshFiles(g)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
createMessagePanel(g, v, "", "Pushing...")
|
|
||||||
go func() {
|
|
||||||
if output, err := gitPush(); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
} else {
|
|
||||||
closeConfirmationPrompt(g)
|
|
||||||
refreshCommits(g)
|
|
||||||
refreshStatus(g)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
mergeView, err := g.View("main")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !file.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "This file has no merge conflicts")
|
|
||||||
}
|
|
||||||
switchFocus(g, v, mergeView)
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
output, err := gitAbortMerge()
|
|
||||||
if err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
createMessagePanel(g, v, "", "Merge aborted")
|
|
||||||
refreshStatus(g)
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := gitResetHard(); err != nil {
|
|
||||||
createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return refreshFiles(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
529
gitcommands.go
529
gitcommands.go
@ -1,529 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
|
|
||||||
// "log"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
|
||||||
git "gopkg.in/src-d/go-git.v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNoOpenCommand : When we don't know which command to use to open a file
|
|
||||||
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GitFile : A staged/unstaged file
|
|
||||||
// TODO: decide whether to give all of these the Git prefix
|
|
||||||
type GitFile struct {
|
|
||||||
Name string
|
|
||||||
HasStagedChanges bool
|
|
||||||
HasUnstagedChanges bool
|
|
||||||
Tracked bool
|
|
||||||
Deleted bool
|
|
||||||
HasMergeConflicts bool
|
|
||||||
DisplayString string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit : A git commit
|
|
||||||
type Commit struct {
|
|
||||||
Sha string
|
|
||||||
Name string
|
|
||||||
Pushed bool
|
|
||||||
DisplayString string
|
|
||||||
}
|
|
||||||
|
|
||||||
// StashEntry : A git stash entry
|
|
||||||
type StashEntry struct {
|
|
||||||
Index int
|
|
||||||
Name string
|
|
||||||
DisplayString string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map (from https://gobyexample.com/collection-functions)
|
|
||||||
func Map(vs []string, f func(string) string) []string {
|
|
||||||
vsm := make([]string, len(vs))
|
|
||||||
for i, v := range vs {
|
|
||||||
vsm[i] = f(v)
|
|
||||||
}
|
|
||||||
return vsm
|
|
||||||
}
|
|
||||||
|
|
||||||
func includesString(list []string, a string) bool {
|
|
||||||
for _, b := range list {
|
|
||||||
if b == a {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure how to genericise this because []interface{} doesn't accept e.g.
|
|
||||||
// []int arguments
|
|
||||||
func includesInt(list []int, a int) bool {
|
|
||||||
for _, b := range list {
|
|
||||||
if b == a {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
|
|
||||||
if len(oldGitFiles) == 0 {
|
|
||||||
return newGitFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
appendedIndexes := make([]int, 0)
|
|
||||||
|
|
||||||
// retain position of files we already could see
|
|
||||||
result := make([]GitFile, 0)
|
|
||||||
for _, oldGitFile := range oldGitFiles {
|
|
||||||
for newIndex, newGitFile := range newGitFiles {
|
|
||||||
if oldGitFile.Name == newGitFile.Name {
|
|
||||||
result = append(result, newGitFile)
|
|
||||||
appendedIndexes = append(appendedIndexes, newIndex)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append any new files to the end
|
|
||||||
for index, newGitFile := range newGitFiles {
|
|
||||||
if !includesInt(appendedIndexes, index) {
|
|
||||||
result = append(result, newGitFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// only to be used when you're already in an error state
|
|
||||||
func runDirectCommandIgnoringError(command string) string {
|
|
||||||
output, _ := runDirectCommand(command)
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDirectCommand(command string) (string, error) {
|
|
||||||
commandLog(command)
|
|
||||||
|
|
||||||
cmdOut, err := exec.
|
|
||||||
Command(state.Platform.shell, state.Platform.shellArg, command).
|
|
||||||
CombinedOutput()
|
|
||||||
return sanitisedCommandOutput(cmdOut, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func branchStringParts(branchString string) (string, string) {
|
|
||||||
// expect string to be something like '4w master`
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: DRY up this function and getGitBranches
|
|
||||||
func getGitStashEntries() []StashEntry {
|
|
||||||
stashEntries := make([]StashEntry, 0)
|
|
||||||
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
|
|
||||||
for i, line := range splitLines(rawString) {
|
|
||||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
|
||||||
}
|
|
||||||
return stashEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
func stashEntryFromLine(line string, index int) StashEntry {
|
|
||||||
return StashEntry{
|
|
||||||
Name: line,
|
|
||||||
Index: index,
|
|
||||||
DisplayString: line,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStashEntryDiff(index int) (string, error) {
|
|
||||||
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 {
|
|
||||||
statusOutput, _ := getGitStatus()
|
|
||||||
statusStrings := splitLines(statusOutput)
|
|
||||||
gitFiles := make([]GitFile, 0)
|
|
||||||
|
|
||||||
for _, statusString := range statusStrings {
|
|
||||||
change := statusString[0:2]
|
|
||||||
stagedChange := change[0:1]
|
|
||||||
unstagedChange := statusString[1:2]
|
|
||||||
filename := statusString[3:]
|
|
||||||
tracked := !includes([]string{"??", "A "}, change)
|
|
||||||
gitFile := GitFile{
|
|
||||||
Name: filename,
|
|
||||||
DisplayString: statusString,
|
|
||||||
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
|
|
||||||
HasUnstagedChanges: unstagedChange != " ",
|
|
||||||
Tracked: tracked,
|
|
||||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
|
||||||
HasMergeConflicts: change == "UU",
|
|
||||||
}
|
|
||||||
gitFiles = append(gitFiles, gitFile)
|
|
||||||
}
|
|
||||||
objectLog(gitFiles)
|
|
||||||
return gitFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitStashDo(index int, method string) (string, error) {
|
|
||||||
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitStashSave(message string) (string, error) {
|
|
||||||
output, err := runCommand("git stash save \"" + message + "\"")
|
|
||||||
if err != nil {
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
// if there are no local changes to save, the exit code is 0, but we want
|
|
||||||
// to raise an error
|
|
||||||
if output == "No local changes to save\n" {
|
|
||||||
return output, errors.New(output)
|
|
||||||
}
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCheckout(branch string, force bool) (string, error) {
|
|
||||||
forceArg := ""
|
|
||||||
if force {
|
|
||||||
forceArg = "--force "
|
|
||||||
}
|
|
||||||
return runCommand("git checkout " + forceArg + branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
|
||||||
outputString := string(output)
|
|
||||||
if outputString == "" && err != nil {
|
|
||||||
return err.Error(), err
|
|
||||||
}
|
|
||||||
return outputString, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCommand(command string) (string, error) {
|
|
||||||
commandLog(command)
|
|
||||||
splitCmd := strings.Split(command, " ")
|
|
||||||
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
|
|
||||||
return sanitisedCommandOutput(cmdOut, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) {
|
|
||||||
return runCommand("code -r " + filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) {
|
|
||||||
return runCommand("subl " + filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func openFile(g *gocui.Gui, filename string) (string, error) {
|
|
||||||
cmdName, cmdTrail, err := getOpenCommand()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return runCommand(cmdName + " " + filename + cmdTrail)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOpenCommand() (string, string, error) {
|
|
||||||
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
|
|
||||||
trailMap := map[string]string{
|
|
||||||
"xdg-open": " &>/dev/null &",
|
|
||||||
"cygstart": "",
|
|
||||||
"open": "",
|
|
||||||
}
|
|
||||||
for name, trail := range trailMap {
|
|
||||||
if out, _ := runCommand("which " + name); out != "exit status 1" {
|
|
||||||
return name, trail, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", "", ErrNoOpenCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitAddPatch(g *gocui.Gui, filename string) {
|
|
||||||
runSubProcess(g, "git", "add", "--patch", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func editFile(g *gocui.Gui, filename string) (string, error) {
|
|
||||||
editor, _ := gitconfig.Global("core.editor")
|
|
||||||
if editor == "" {
|
|
||||||
editor = os.Getenv("VISUAL")
|
|
||||||
}
|
|
||||||
if editor == "" {
|
|
||||||
editor = os.Getenv("EDITOR")
|
|
||||||
}
|
|
||||||
if editor == "" {
|
|
||||||
if _, err := runCommand("which vi"); err == nil {
|
|
||||||
editor = "vi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if editor == "" {
|
|
||||||
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
|
|
||||||
}
|
|
||||||
runSubProcess(g, editor, filename)
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
|
|
||||||
subprocess = exec.Command(cmdName, commandArgs...)
|
|
||||||
subprocess.Stdin = os.Stdin
|
|
||||||
subprocess.Stdout = os.Stdout
|
|
||||||
subprocess.Stderr = os.Stderr
|
|
||||||
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
return ErrSubprocess
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBranchGraph(branch string) (string, error) {
|
|
||||||
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyInGitRepo() {
|
|
||||||
if output, err := runCommand("git status"); err != nil {
|
|
||||||
fmt.Println(output)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCommits() []Commit {
|
|
||||||
pushables := gitCommitsToPush()
|
|
||||||
log := getLog()
|
|
||||||
commits := make([]Commit, 0)
|
|
||||||
// now we can split it up and turn it into commits
|
|
||||||
lines := splitLines(log)
|
|
||||||
for _, line := range lines {
|
|
||||||
splitLine := strings.Split(line, " ")
|
|
||||||
sha := splitLine[0]
|
|
||||||
pushed := includesString(pushables, sha)
|
|
||||||
commits = append(commits, Commit{
|
|
||||||
Sha: sha,
|
|
||||||
Name: strings.Join(splitLine[1:], " "),
|
|
||||||
Pushed: pushed,
|
|
||||||
DisplayString: strings.Join(splitLine, " "),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return commits
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLog() string {
|
|
||||||
// currently limiting to 30 for performance reasons
|
|
||||||
// TODO: add lazyloading when you scroll down
|
|
||||||
result, err := runDirectCommand("git log --oneline -30")
|
|
||||||
if err != nil {
|
|
||||||
// assume if there is an error there are no commits yet for this branch
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitIgnore(filename string) {
|
|
||||||
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitShow(sha string) string {
|
|
||||||
result, err := runDirectCommand("git show --color " + sha)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDiff(file GitFile) string {
|
|
||||||
cachedArg := ""
|
|
||||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
|
||||||
cachedArg = "--cached "
|
|
||||||
}
|
|
||||||
deletedArg := ""
|
|
||||||
if file.Deleted {
|
|
||||||
deletedArg = "-- "
|
|
||||||
}
|
|
||||||
trackedArg := ""
|
|
||||||
if !file.Tracked && !file.HasStagedChanges {
|
|
||||||
trackedArg = "--no-index /dev/null "
|
|
||||||
}
|
|
||||||
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
|
|
||||||
// for now we assume an error means the file was deleted
|
|
||||||
s, _ := runCommand(command)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func catFile(file string) (string, error) {
|
|
||||||
return runDirectCommand("cat " + file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stageFile(file string) error {
|
|
||||||
_, err := runCommand("git add " + file)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func unStageFile(file string, tracked bool) error {
|
|
||||||
var command string
|
|
||||||
if tracked {
|
|
||||||
command = "git reset HEAD "
|
|
||||||
} else {
|
|
||||||
command = "git rm --cached "
|
|
||||||
}
|
|
||||||
_, err := runCommand(command + file)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGitStatus() (string, error) {
|
|
||||||
return runCommand("git status --untracked-files=all --short")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isInMergeState() (bool, error) {
|
|
||||||
output, err := runCommand("git status --untracked-files=all")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFile(file GitFile) error {
|
|
||||||
// if the file isn't tracked, we assume you want to delete it
|
|
||||||
if !file.Tracked {
|
|
||||||
_, err := runCommand("rm -rf ./" + file.Name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// if the file is tracked, we assume you want to just check it out
|
|
||||||
_, err := runCommand("git checkout " + file.Name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCommit(g *gocui.Gui, message string) (string, error) {
|
|
||||||
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
|
||||||
if gpgsign != "" {
|
|
||||||
runSubProcess(g, "git", "commit")
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return runDirectCommand("git commit -m \"" + message + "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitPull() (string, error) {
|
|
||||||
return runDirectCommand("git pull --no-edit")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitPush() (string, error) {
|
|
||||||
return runDirectCommand("git push -u origin " + state.Branches[0].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitSquashPreviousTwoCommits(message string) (string, error) {
|
|
||||||
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitSquashFixupCommit(branchName string, shaValue string) (string, error) {
|
|
||||||
var err error
|
|
||||||
commands := []string{
|
|
||||||
"git checkout -q " + shaValue,
|
|
||||||
"git reset --soft " + shaValue + "^",
|
|
||||||
"git commit --amend -C " + shaValue + "^",
|
|
||||||
"git rebase --onto HEAD " + shaValue + " " + branchName,
|
|
||||||
}
|
|
||||||
ret := ""
|
|
||||||
for _, command := range commands {
|
|
||||||
devLog(command)
|
|
||||||
output, err := runDirectCommand(command)
|
|
||||||
ret += output
|
|
||||||
if err != nil {
|
|
||||||
devLog(ret)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// We are already in an error state here so we're just going to append
|
|
||||||
// the output of these commands
|
|
||||||
ret += runDirectCommandIgnoringError("git branch -d " + shaValue)
|
|
||||||
ret += runDirectCommandIgnoringError("git checkout " + branchName)
|
|
||||||
}
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitRenameCommit(message string) (string, error) {
|
|
||||||
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitFetch() (string, error) {
|
|
||||||
return runDirectCommand("git fetch")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitResetToCommit(sha string) (string, error) {
|
|
||||||
return runDirectCommand("git reset " + sha)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitNewBranch(name string) (string, error) {
|
|
||||||
return runDirectCommand("git checkout -b " + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitDeleteBranch(branch string) (string, error) {
|
|
||||||
return runCommand("git branch -d " + branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitListStash() (string, error) {
|
|
||||||
return runDirectCommand("git stash list")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitMerge(branchName string) (string, error) {
|
|
||||||
return runDirectCommand("git merge --no-edit " + branchName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitAbortMerge() (string, error) {
|
|
||||||
return runDirectCommand("git merge --abort")
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitUpstreamDifferenceCount() (string, string) {
|
|
||||||
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
|
||||||
if err != nil {
|
|
||||||
return "?", "?"
|
|
||||||
}
|
|
||||||
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
|
|
||||||
if err != nil {
|
|
||||||
return "?", "?"
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCommitsToPush() []string {
|
|
||||||
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
|
|
||||||
if err != nil {
|
|
||||||
return make([]string, 0)
|
|
||||||
}
|
|
||||||
return splitLines(pushables)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGitBranches() []Branch {
|
|
||||||
builder := newBranchListBuilder()
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func branchIncluded(branchName string, branches []Branch) bool {
|
|
||||||
for _, existingBranch := range branches {
|
|
||||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitResetHard() error {
|
|
||||||
return w.Reset(&git.ResetOptions{Mode: git.HardReset})
|
|
||||||
}
|
|
94
main.go
94
main.go
@ -1,36 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/jesseduffield/lazygit/pkg/app"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
git "gopkg.in/src-d/go-git.v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrSubProcess is raised when we are running a subprocess
|
|
||||||
var (
|
var (
|
||||||
ErrSubprocess = errors.New("running subprocess")
|
|
||||||
subprocess *exec.Cmd
|
|
||||||
|
|
||||||
commit string
|
commit string
|
||||||
version = "unversioned"
|
version = "unversioned"
|
||||||
|
date string
|
||||||
|
|
||||||
date string
|
|
||||||
debuggingFlag = flag.Bool("debug", false, "a boolean")
|
debuggingFlag = flag.Bool("debug", false, "a boolean")
|
||||||
versionFlag = flag.Bool("v", false, "Print the current version")
|
versionFlag = flag.Bool("v", false, "Print the current version")
|
||||||
|
|
||||||
w *git.Worktree
|
|
||||||
r *git.Repository
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func homeDirectory() string {
|
func homeDirectory() string {
|
||||||
@ -46,46 +35,6 @@ func projectPath(path string) string {
|
|||||||
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func devLog(objects ...interface{}) {
|
|
||||||
localLog("development.log", objects...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func objectLog(object interface{}) {
|
|
||||||
if !*debuggingFlag {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
str := spew.Sdump(object)
|
|
||||||
localLog("development.log", str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandLog(objects ...interface{}) {
|
|
||||||
localLog("commands.log", objects...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func localLog(path string, objects ...interface{}) {
|
|
||||||
if !*debuggingFlag {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.OpenFile(projectPath(path), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
log.SetOutput(f)
|
|
||||||
for _, object := range objects {
|
|
||||||
log.Println(fmt.Sprint(object))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when building the binary, `version` is set as a compile-time variable, along
|
// when building the binary, `version` is set as a compile-time variable, along
|
||||||
// with `date` and `commit`. If this program has been opened directly via go,
|
// with `date` and `commit`. If this program has been opened directly via go,
|
||||||
// we will populate the `version` with VERSION in the lazygit root directory
|
// we will populate the `version` with VERSION in the lazygit root directory
|
||||||
@ -98,21 +47,7 @@ func fallbackVersion() string {
|
|||||||
return string(byteVersion)
|
return string(byteVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupWorktree() {
|
|
||||||
var err error
|
|
||||||
r, err = git.PlainOpen(".")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err = r.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
devLog("\n\n\n\n\n\n\n\n\n\n")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if version == "unversioned" {
|
if version == "unversioned" {
|
||||||
version = fallbackVersion()
|
version = fallbackVersion()
|
||||||
@ -121,18 +56,15 @@ func main() {
|
|||||||
fmt.Printf("commit=%s, build date=%s, version=%s\n", commit, date, version)
|
fmt.Printf("commit=%s, build date=%s, version=%s\n", commit, date, version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
verifyInGitRepo()
|
appConfig := &config.AppConfig{
|
||||||
navigateToRepoRootDirectory()
|
Name: "lazygit",
|
||||||
setupWorktree()
|
Version: version,
|
||||||
for {
|
Commit: commit,
|
||||||
if err := run(); err != nil {
|
BuildDate: date,
|
||||||
if err == gocui.ErrQuit {
|
Debug: *debuggingFlag,
|
||||||
break
|
|
||||||
} else if err == ErrSubprocess {
|
|
||||||
subprocess.Run()
|
|
||||||
} else {
|
|
||||||
log.Panicln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
app, err := app.NewApp(appConfig)
|
||||||
|
app.Log.Info(err)
|
||||||
|
app.GitCommand.SetupGit()
|
||||||
|
app.Gui.RunWithSubprocesses()
|
||||||
}
|
}
|
||||||
|
263
merge_panel.go
263
merge_panel.go
@ -1,263 +0,0 @@
|
|||||||
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func findConflicts(content string) ([]conflict, error) {
|
|
||||||
conflicts := make([]conflict, 0)
|
|
||||||
var newConflict conflict
|
|
||||||
for i, line := range splitLines(content) {
|
|
||||||
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
|
||||||
newConflict = conflict{start: i}
|
|
||||||
} else if line == "=======" {
|
|
||||||
newConflict.middle = i
|
|
||||||
} else if strings.HasPrefix(line, ">>>>>>> ") {
|
|
||||||
newConflict.end = i
|
|
||||||
conflicts = append(conflicts, newConflict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return conflicts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
|
|
||||||
return conflicts[0], conflicts[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
|
|
||||||
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
|
||||||
}
|
|
||||||
|
|
||||||
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
|
||||||
if len(conflicts) == 0 {
|
|
||||||
return content, nil
|
|
||||||
}
|
|
||||||
conflict, remainingConflicts := shiftConflict(conflicts)
|
|
||||||
var outputBuffer bytes.Buffer
|
|
||||||
for i, line := range splitLines(content) {
|
|
||||||
colourAttr := color.FgWhite
|
|
||||||
if i == conflict.start || i == conflict.middle || i == conflict.end {
|
|
||||||
colourAttr = color.FgRed
|
|
||||||
}
|
|
||||||
colour := color.New(colourAttr)
|
|
||||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
|
|
||||||
colour.Add(color.Bold)
|
|
||||||
}
|
|
||||||
if i == conflict.end && len(remainingConflicts) > 0 {
|
|
||||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
|
||||||
}
|
|
||||||
outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n")
|
|
||||||
}
|
|
||||||
return outputBuffer.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
state.ConflictTop = true
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
state.ConflictTop = false
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if state.ConflictIndex >= len(state.Conflicts)-1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
state.ConflictIndex++
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if state.ConflictIndex <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
state.ConflictIndex--
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIndexToDelete(i int, conflict conflict, pick string) bool {
|
|
||||||
return i == conflict.middle ||
|
|
||||||
i == conflict.start ||
|
|
||||||
i == conflict.end ||
|
|
||||||
pick != "both" &&
|
|
||||||
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
|
|
||||||
(pick == "top" && i > conflict.middle && i < conflict.end)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
|
|
||||||
gitFile, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := os.Open(gitFile.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
output := ""
|
|
||||||
for i := 0; true; i++ {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !isIndexToDelete(i, conflict, pick) {
|
|
||||||
output += line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
devLog(output)
|
|
||||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushFileSnapshot(g *gocui.Gui) error {
|
|
||||||
gitFile, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
content, err := catFile(gitFile.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
state.EditHistory.Push(content)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if state.EditHistory.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
prevContent := state.EditHistory.Pop().(string)
|
|
||||||
gitFile, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
conflict := state.Conflicts[state.ConflictIndex]
|
|
||||||
pushFileSnapshot(g)
|
|
||||||
pick := "bottom"
|
|
||||||
if state.ConflictTop {
|
|
||||||
pick = "top"
|
|
||||||
}
|
|
||||||
err := resolveConflict(g, conflict, pick)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
refreshMergePanel(g)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
conflict := state.Conflicts[state.ConflictIndex]
|
|
||||||
pushFileSnapshot(g)
|
|
||||||
err := resolveConflict(g, conflict, "both")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentViewName(g *gocui.Gui) string {
|
|
||||||
currentView := g.CurrentView()
|
|
||||||
return currentView.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshMergePanel(g *gocui.Gui) error {
|
|
||||||
cat, err := catSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
state.Conflicts, err = findConflicts(cat)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.Conflicts) == 0 {
|
|
||||||
return handleCompleteMerge(g)
|
|
||||||
} else if state.ConflictIndex > len(state.Conflicts)-1 {
|
|
||||||
state.ConflictIndex = len(state.Conflicts) - 1
|
|
||||||
}
|
|
||||||
hasFocus := currentViewName(g) == "main"
|
|
||||||
if hasFocus {
|
|
||||||
renderMergeOptions(g)
|
|
||||||
}
|
|
||||||
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := scrollToConflict(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return renderString(g, "main", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollToConflict(g *gocui.Gui) error {
|
|
||||||
mainView, err := g.View("main")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(state.Conflicts) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
conflict := state.Conflicts[state.ConflictIndex]
|
|
||||||
ox, _ := mainView.Origin()
|
|
||||||
_, height := mainView.Size()
|
|
||||||
conflictMiddle := (conflict.end + conflict.start) / 2
|
|
||||||
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
|
||||||
return mainView.SetOrigin(ox, newOriginY)
|
|
||||||
}
|
|
||||||
|
|
||||||
func switchToMerging(g *gocui.Gui) error {
|
|
||||||
state.ConflictIndex = 0
|
|
||||||
state.ConflictTop = true
|
|
||||||
_, err := g.SetCurrentView("main")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderMergeOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"↑ ↓": "select hunk",
|
|
||||||
"← →": "navigate conflicts",
|
|
||||||
"space": "pick hunk",
|
|
||||||
"b": "pick both hunks",
|
|
||||||
"z": "undo",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
refreshFiles(g)
|
|
||||||
return switchFocus(g, v, filesView)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCompleteMerge(g *gocui.Gui) error {
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stageSelectedFile(g)
|
|
||||||
refreshFiles(g)
|
|
||||||
return switchFocus(g, nil, filesView)
|
|
||||||
}
|
|
71
pkg/app/app.go
Normal file
71
pkg/app/app.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct
|
||||||
|
type App struct {
|
||||||
|
closers []io.Closer
|
||||||
|
|
||||||
|
Config config.AppConfigurer
|
||||||
|
Log *logrus.Logger
|
||||||
|
OSCommand *commands.OSCommand
|
||||||
|
GitCommand *commands.GitCommand
|
||||||
|
Gui *gui.Gui
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogger(config config.AppConfigurer) *logrus.Logger {
|
||||||
|
log := logrus.New()
|
||||||
|
if !config.GetDebug() {
|
||||||
|
log.Out = ioutil.Discard
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
|
||||||
|
}
|
||||||
|
log.SetOutput(file)
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp retruns a new applications
|
||||||
|
func NewApp(config config.AppConfigurer) (*App, error) {
|
||||||
|
app := &App{
|
||||||
|
closers: []io.Closer{},
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
app.Log = newLogger(config)
|
||||||
|
app.OSCommand, err = commands.NewOSCommand(app.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, config.GetVersion())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes any resources
|
||||||
|
func (app *App) Close() error {
|
||||||
|
for _, closer := range app.closers {
|
||||||
|
err := closer.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,22 +1,26 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Branch : A git branch
|
// Branch : A git branch
|
||||||
|
// duplicating this for now
|
||||||
type Branch struct {
|
type Branch struct {
|
||||||
Name string
|
Name string
|
||||||
Recency string
|
Recency string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Branch) getDisplayString() string {
|
// GetDisplayString returns the dispaly string of branch
|
||||||
return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor())
|
func (b *Branch) GetDisplayString() string {
|
||||||
|
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Branch) getColor() color.Attribute {
|
// GetColor branch color
|
||||||
|
func (b *Branch) GetColor() color.Attribute {
|
||||||
switch b.getType() {
|
switch b.getType() {
|
||||||
case "feature":
|
case "feature":
|
||||||
return color.FgGreen
|
return color.FgGreen
|
497
pkg/commands/git.go
Normal file
497
pkg/commands/git.go
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||||
|
gogit "gopkg.in/src-d/go-git.v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitCommand is our main git interface
|
||||||
|
type GitCommand struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
OSCommand *OSCommand
|
||||||
|
Worktree *gogit.Worktree
|
||||||
|
Repo *gogit.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGitCommand it runs git commands
|
||||||
|
func NewGitCommand(log *logrus.Logger, osCommand *OSCommand) (*GitCommand, error) {
|
||||||
|
gitCommand := &GitCommand{
|
||||||
|
Log: log,
|
||||||
|
OSCommand: osCommand,
|
||||||
|
}
|
||||||
|
return gitCommand, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupGit sets git repo up
|
||||||
|
func (c *GitCommand) SetupGit() {
|
||||||
|
c.verifyInGitRepo()
|
||||||
|
c.navigateToRepoRootDirectory()
|
||||||
|
c.setupWorktree()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStashEntries stash entryies
|
||||||
|
func (c *GitCommand) GetStashEntries() []StashEntry {
|
||||||
|
stashEntries := make([]StashEntry, 0)
|
||||||
|
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
|
||||||
|
for i, line := range utils.SplitLines(rawString) {
|
||||||
|
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||||
|
}
|
||||||
|
return stashEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
func stashEntryFromLine(line string, index int) StashEntry {
|
||||||
|
return StashEntry{
|
||||||
|
Name: line,
|
||||||
|
Index: index,
|
||||||
|
DisplayString: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStashEntryDiff stash diff
|
||||||
|
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusFiles git status files
|
||||||
|
func (c *GitCommand) GetStatusFiles() []File {
|
||||||
|
statusOutput, _ := c.GitStatus()
|
||||||
|
statusStrings := utils.SplitLines(statusOutput)
|
||||||
|
files := make([]File, 0)
|
||||||
|
|
||||||
|
for _, statusString := range statusStrings {
|
||||||
|
change := statusString[0:2]
|
||||||
|
stagedChange := change[0:1]
|
||||||
|
unstagedChange := statusString[1:2]
|
||||||
|
filename := statusString[3:]
|
||||||
|
tracked := !includes([]string{"??", "A "}, change)
|
||||||
|
file := File{
|
||||||
|
Name: filename,
|
||||||
|
DisplayString: statusString,
|
||||||
|
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
|
||||||
|
HasUnstagedChanges: unstagedChange != " ",
|
||||||
|
Tracked: tracked,
|
||||||
|
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||||
|
HasMergeConflicts: change == "UU",
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
c.Log.Info(files) // TODO: use a dumper-esque log here
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashDo modify stash
|
||||||
|
func (c *GitCommand) StashDo(index int, method string) error {
|
||||||
|
return c.OSCommand.RunCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashSave save stash
|
||||||
|
// TODO: before calling this, check if there is anything to save
|
||||||
|
func (c *GitCommand) StashSave(message string) error {
|
||||||
|
return c.OSCommand.RunCommand("git stash save " + c.OSCommand.Quote(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeStatusFiles merge status files
|
||||||
|
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
|
||||||
|
if len(oldFiles) == 0 {
|
||||||
|
return newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
appendedIndexes := make([]int, 0)
|
||||||
|
|
||||||
|
// retain position of files we already could see
|
||||||
|
result := make([]File, 0)
|
||||||
|
for _, oldFile := range oldFiles {
|
||||||
|
for newIndex, newFile := range newFiles {
|
||||||
|
if oldFile.Name == newFile.Name {
|
||||||
|
result = append(result, newFile)
|
||||||
|
appendedIndexes = append(appendedIndexes, newIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append any new files to the end
|
||||||
|
for index, newFile := range newFiles {
|
||||||
|
if !includesInt(appendedIndexes, index) {
|
||||||
|
result = append(result, newFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) verifyInGitRepo() {
|
||||||
|
if output, err := c.OSCommand.RunCommandWithOutput("git status"); err != nil {
|
||||||
|
fmt.Println(output)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBranchName branch name
|
||||||
|
func (c *GitCommand) GetBranchName() (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) navigateToRepoRootDirectory() {
|
||||||
|
_, err := os.Stat(".git")
|
||||||
|
for os.IsNotExist(err) {
|
||||||
|
c.Log.Debug("going up a directory to find the root")
|
||||||
|
os.Chdir("..")
|
||||||
|
_, err = os.Stat(".git")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) setupWorktree() {
|
||||||
|
r, err := gogit.PlainOpen(".")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
c.Repo = r
|
||||||
|
|
||||||
|
w, err := r.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
c.Worktree = w
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetHard does the equivalent of `git reset --hard HEAD`
|
||||||
|
func (c *GitCommand) ResetHard() error {
|
||||||
|
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
|
||||||
|
// current branch
|
||||||
|
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
|
||||||
|
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
|
||||||
|
if err != nil {
|
||||||
|
return "?", "?"
|
||||||
|
}
|
||||||
|
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
|
||||||
|
if err != nil {
|
||||||
|
return "?", "?"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
|
||||||
|
// to the remote branch of the current branch
|
||||||
|
func (c *GitCommand) GetCommitsToPush() []string {
|
||||||
|
pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
|
||||||
|
if err != nil {
|
||||||
|
return make([]string, 0)
|
||||||
|
}
|
||||||
|
return utils.SplitLines(pushables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameCommit renames the topmost commit with the given name
|
||||||
|
func (c *GitCommand) RenameCommit(name string) error {
|
||||||
|
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m " + c.OSCommand.Quote(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fetch git repo
|
||||||
|
func (c *GitCommand) Fetch() error {
|
||||||
|
return c.OSCommand.RunCommand("git fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetToCommit reset to commit
|
||||||
|
func (c *GitCommand) ResetToCommit(sha string) error {
|
||||||
|
return c.OSCommand.RunCommand("git reset " + sha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBranch create new branch
|
||||||
|
func (c *GitCommand) NewBranch(name string) error {
|
||||||
|
return c.OSCommand.RunCommand("git checkout -b " + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBranch delete branch
|
||||||
|
func (c *GitCommand) DeleteBranch(branch string) error {
|
||||||
|
return c.OSCommand.RunCommand("git branch -d " + branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListStash list stash
|
||||||
|
func (c *GitCommand) ListStash() (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("git stash list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merge
|
||||||
|
func (c *GitCommand) Merge(branchName string) error {
|
||||||
|
return c.OSCommand.RunCommand("git merge --no-edit " + branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortMerge abort merge
|
||||||
|
func (c *GitCommand) AbortMerge() error {
|
||||||
|
return c.OSCommand.RunCommand("git merge --abort")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||||
|
// whether we need to run a subprocess to allow them to enter their password
|
||||||
|
func (c *GitCommand) UsingGpg() bool {
|
||||||
|
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
||||||
|
if gpgsign == "" {
|
||||||
|
gpgsign, _ = gitconfig.Local("commit.gpgsign")
|
||||||
|
}
|
||||||
|
if gpgsign == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit commit to git
|
||||||
|
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) {
|
||||||
|
command := "git commit -m " + c.OSCommand.Quote(message)
|
||||||
|
if c.UsingGpg() {
|
||||||
|
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)
|
||||||
|
}
|
||||||
|
return nil, c.OSCommand.RunCommand(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull pull from repo
|
||||||
|
func (c *GitCommand) Pull() error {
|
||||||
|
return c.OSCommand.RunCommand("git pull --no-edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push push to a branch
|
||||||
|
func (c *GitCommand) Push(branchName string) error {
|
||||||
|
return c.OSCommand.RunCommand("git push -u origin " + branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SquashPreviousTwoCommits squashes a commit down to the one below it
|
||||||
|
// retaining the message of the higher commit
|
||||||
|
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
|
||||||
|
// TODO: test this
|
||||||
|
err := c.OSCommand.RunCommand("git reset --soft HEAD^")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: if password is required, we need to return a subprocess
|
||||||
|
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
|
||||||
|
// retaining the commit message of the lower commit
|
||||||
|
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
|
||||||
|
var err error
|
||||||
|
commands := []string{
|
||||||
|
"git checkout -q " + shaValue,
|
||||||
|
"git reset --soft " + shaValue + "^",
|
||||||
|
"git commit --amend -C " + shaValue + "^",
|
||||||
|
"git rebase --onto HEAD " + shaValue + " " + branchName,
|
||||||
|
}
|
||||||
|
ret := ""
|
||||||
|
for _, command := range commands {
|
||||||
|
c.Log.Info(command)
|
||||||
|
output, err := c.OSCommand.RunCommandWithOutput(command)
|
||||||
|
ret += output
|
||||||
|
if err != nil {
|
||||||
|
c.Log.Info(ret)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// We are already in an error state here so we're just going to append
|
||||||
|
// the output of these commands
|
||||||
|
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
|
||||||
|
ret += output
|
||||||
|
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
|
||||||
|
ret += output
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(ret)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatFile obtain the contents of a file
|
||||||
|
func (c *GitCommand) CatFile(file string) (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("cat " + file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StageFile stages a file
|
||||||
|
func (c *GitCommand) StageFile(file string) error {
|
||||||
|
return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnStageFile unstages a file
|
||||||
|
func (c *GitCommand) UnStageFile(file string, tracked bool) error {
|
||||||
|
var command string
|
||||||
|
if tracked {
|
||||||
|
command = "git reset HEAD "
|
||||||
|
} else {
|
||||||
|
command = "git rm --cached "
|
||||||
|
}
|
||||||
|
return c.OSCommand.RunCommand(command + file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitStatus returns the plaintext short status of the repo
|
||||||
|
func (c *GitCommand) GitStatus() (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInMergeState states whether we are still mid-merge
|
||||||
|
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||||
|
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveFile directly
|
||||||
|
func (c *GitCommand) RemoveFile(file File) error {
|
||||||
|
// if the file isn't tracked, we assume you want to delete it
|
||||||
|
if !file.Tracked {
|
||||||
|
return c.OSCommand.RunCommand("rm -rf ./" + file.Name)
|
||||||
|
}
|
||||||
|
// if the file is tracked, we assume you want to just check it out
|
||||||
|
return c.OSCommand.RunCommand("git checkout " + file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout checks out a branch, with --force if you set the force arg to true
|
||||||
|
func (c *GitCommand) Checkout(branch string, force bool) error {
|
||||||
|
forceArg := ""
|
||||||
|
if force {
|
||||||
|
forceArg = "--force "
|
||||||
|
}
|
||||||
|
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPatch prepares a subprocess for adding a patch by patch
|
||||||
|
// this will eventually be swapped out for a better solution inside the Gui
|
||||||
|
func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) {
|
||||||
|
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||||
|
func (c *GitCommand) PrepareCommitSubProcess() (*exec.Cmd, error) {
|
||||||
|
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||||
|
// Currently it limits the result to 100 commits, but when we get async stuff
|
||||||
|
// working we can do lazy loading
|
||||||
|
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||||
|
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map (from https://gobyexample.com/collection-functions)
|
||||||
|
func Map(vs []string, f func(string) string) []string {
|
||||||
|
vsm := make([]string, len(vs))
|
||||||
|
for i, v := range vs {
|
||||||
|
vsm[i] = f(v)
|
||||||
|
}
|
||||||
|
return vsm
|
||||||
|
}
|
||||||
|
|
||||||
|
func includesString(list []string, a string) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// not sure how to genericise this because []interface{} doesn't accept e.g.
|
||||||
|
// []int arguments
|
||||||
|
func includesInt(list []int, a int) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommits obtains the commits of the current branch
|
||||||
|
func (c *GitCommand) GetCommits() []Commit {
|
||||||
|
pushables := c.GetCommitsToPush()
|
||||||
|
log := c.GetLog()
|
||||||
|
commits := make([]Commit, 0)
|
||||||
|
// now we can split it up and turn it into commits
|
||||||
|
lines := utils.SplitLines(log)
|
||||||
|
for _, line := range lines {
|
||||||
|
splitLine := strings.Split(line, " ")
|
||||||
|
sha := splitLine[0]
|
||||||
|
pushed := includesString(pushables, sha)
|
||||||
|
commits = append(commits, Commit{
|
||||||
|
Sha: sha,
|
||||||
|
Name: strings.Join(splitLine[1:], " "),
|
||||||
|
Pushed: pushed,
|
||||||
|
DisplayString: strings.Join(splitLine, " "),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLog gets the git log (currently limited to 30 commits for performance
|
||||||
|
// until we work out lazy loading
|
||||||
|
func (c *GitCommand) GetLog() string {
|
||||||
|
// currently limiting to 30 for performance reasons
|
||||||
|
// TODO: add lazyloading when you scroll down
|
||||||
|
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
|
||||||
|
if err != nil {
|
||||||
|
// assume if there is an error there are no commits yet for this branch
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore adds a file to the gitignore for the repo
|
||||||
|
func (c *GitCommand) Ignore(filename string) {
|
||||||
|
if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show shows the diff of a commit
|
||||||
|
func (c *GitCommand) Show(sha string) string {
|
||||||
|
result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff returns the diff of a file
|
||||||
|
func (c *GitCommand) Diff(file File) string {
|
||||||
|
cachedArg := ""
|
||||||
|
fileName := file.Name
|
||||||
|
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||||
|
cachedArg = "--cached"
|
||||||
|
} else {
|
||||||
|
// if the file is staged and has spaces in it, it comes pre-quoted
|
||||||
|
fileName = c.OSCommand.Quote(fileName)
|
||||||
|
}
|
||||||
|
deletedArg := ""
|
||||||
|
if file.Deleted {
|
||||||
|
deletedArg = "--"
|
||||||
|
}
|
||||||
|
trackedArg := ""
|
||||||
|
if !file.Tracked && !file.HasStagedChanges {
|
||||||
|
trackedArg = "--no-index /dev/null"
|
||||||
|
}
|
||||||
|
command := fmt.Sprintf("%s %s %s %s %s", "git diff --color ", cachedArg, deletedArg, trackedArg, fileName)
|
||||||
|
|
||||||
|
// for now we assume an error means the file was deleted
|
||||||
|
s, _ := c.OSCommand.RunCommandWithOutput(command)
|
||||||
|
return s
|
||||||
|
}
|
36
pkg/commands/git_structs.go
Normal file
36
pkg/commands/git_structs.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
// File : A staged/unstaged file
|
||||||
|
// TODO: decide whether to give all of these the Git prefix
|
||||||
|
type File struct {
|
||||||
|
Name string
|
||||||
|
HasStagedChanges bool
|
||||||
|
HasUnstagedChanges bool
|
||||||
|
Tracked bool
|
||||||
|
Deleted bool
|
||||||
|
HasMergeConflicts bool
|
||||||
|
DisplayString string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit : A git commit
|
||||||
|
type Commit struct {
|
||||||
|
Sha string
|
||||||
|
Name string
|
||||||
|
Pushed bool
|
||||||
|
DisplayString string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashEntry : A git stash entry
|
||||||
|
type StashEntry struct {
|
||||||
|
Index int
|
||||||
|
Name string
|
||||||
|
DisplayString string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||||
|
// numbers in the file where the conflict bars appear
|
||||||
|
type Conflict struct {
|
||||||
|
Start int
|
||||||
|
Middle int
|
||||||
|
End int
|
||||||
|
}
|
174
pkg/commands/os.go
Normal file
174
pkg/commands/os.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/mgutz/str"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoOpenCommand : When we don't know which command to use to open a file
|
||||||
|
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
|
||||||
|
// ErrNoEditorDefined : When we can't find an editor to edit a file
|
||||||
|
ErrNoEditorDefined = errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Platform stores the os state
|
||||||
|
type Platform struct {
|
||||||
|
os string
|
||||||
|
shell string
|
||||||
|
shellArg string
|
||||||
|
escapedQuote string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSCommand holds all the os commands
|
||||||
|
type OSCommand struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Platform *Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOSCommand os command runner
|
||||||
|
func NewOSCommand(log *logrus.Logger) (*OSCommand, error) {
|
||||||
|
osCommand := &OSCommand{
|
||||||
|
Log: log,
|
||||||
|
Platform: getPlatform(),
|
||||||
|
}
|
||||||
|
return osCommand, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||||
|
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
||||||
|
c.Log.WithField("command", command).Info("RunCommand")
|
||||||
|
splitCmd := str.ToArgv(command)
|
||||||
|
c.Log.Info(splitCmd)
|
||||||
|
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
|
||||||
|
return sanitisedCommandOutput(cmdOut, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand runs a command and just returns the error
|
||||||
|
func (c *OSCommand) RunCommand(command string) error {
|
||||||
|
_, err := c.RunCommandWithOutput(command)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunDirectCommand wrapper around direct commands
|
||||||
|
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||||
|
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||||
|
args := str.ToArgv(c.Platform.shellArg + " " + command)
|
||||||
|
c.Log.Info(spew.Sdump(args))
|
||||||
|
|
||||||
|
cmdOut, err := exec.
|
||||||
|
Command(c.Platform.shell, args...).
|
||||||
|
CombinedOutput()
|
||||||
|
return sanitisedCommandOutput(cmdOut, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||||
|
outputString := string(output)
|
||||||
|
if err != nil {
|
||||||
|
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||||
|
// from the combined output
|
||||||
|
return outputString, errors.New(outputString)
|
||||||
|
}
|
||||||
|
return outputString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlatform() *Platform {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return &Platform{
|
||||||
|
os: "windows",
|
||||||
|
shell: "cmd",
|
||||||
|
shellArg: "/c",
|
||||||
|
escapedQuote: "\\\"",
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return &Platform{
|
||||||
|
os: runtime.GOOS,
|
||||||
|
shell: "bash",
|
||||||
|
shellArg: "-c",
|
||||||
|
escapedQuote: "\"",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOpenCommand get open command
|
||||||
|
func (c *OSCommand) GetOpenCommand() (string, string, error) {
|
||||||
|
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
|
||||||
|
trailMap := map[string]string{
|
||||||
|
"xdg-open": " &>/dev/null &",
|
||||||
|
"cygstart": "",
|
||||||
|
"open": "",
|
||||||
|
}
|
||||||
|
for name, trail := range trailMap {
|
||||||
|
if err := c.RunCommand("which " + name); err == nil {
|
||||||
|
return name, trail, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", ErrNoOpenCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// VsCodeOpenFile opens the file in code, with the -r flag to open in the
|
||||||
|
// current window
|
||||||
|
// each of these open files needs to have the same function signature because
|
||||||
|
// they're being passed as arguments into another function,
|
||||||
|
// but only editFile actually returns a *exec.Cmd
|
||||||
|
func (c *OSCommand) VsCodeOpenFile(filename string) (*exec.Cmd, error) {
|
||||||
|
return nil, c.RunCommand("code -r " + filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SublimeOpenFile opens the filein sublime
|
||||||
|
// may be deprecated in the future
|
||||||
|
func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) {
|
||||||
|
return nil, c.RunCommand("subl " + filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFile opens a file with the given
|
||||||
|
func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) {
|
||||||
|
cmdName, cmdTrail, err := c.GetOpenCommand()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = c.RunCommand(cmdName + " " + filename + cmdTrail) // TODO: test on linux
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||||
|
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||||
|
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||||
|
editor, _ := gitconfig.Global("core.editor")
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("VISUAL")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("EDITOR")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
if err := c.RunCommand("which vi"); err == nil {
|
||||||
|
editor = "vi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
return nil, ErrNoEditorDefined
|
||||||
|
}
|
||||||
|
return c.PrepareSubProcess(editor, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||||
|
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
|
||||||
|
subprocess := exec.Command(cmdName, commandArgs...)
|
||||||
|
return subprocess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote wraps a message in platform-specific quotation marks
|
||||||
|
func (c *OSCommand) Quote(message string) string {
|
||||||
|
return c.Platform.escapedQuote + message + c.Platform.escapedQuote
|
||||||
|
}
|
45
pkg/config/app_config.go
Normal file
45
pkg/config/app_config.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// AppConfig contains the base configuration fields required for lazygit.
|
||||||
|
type AppConfig struct {
|
||||||
|
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||||
|
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||||
|
Commit string `long:"commit" env:"COMMIT"`
|
||||||
|
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||||
|
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||||
|
// from AppConfig and still be used by lazygit.
|
||||||
|
type AppConfigurer interface {
|
||||||
|
GetDebug() bool
|
||||||
|
GetVersion() string
|
||||||
|
GetCommit() string
|
||||||
|
GetBuildDate() string
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDebug returns debug flag
|
||||||
|
func (c *AppConfig) GetDebug() bool {
|
||||||
|
return c.Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion returns debug flag
|
||||||
|
func (c *AppConfig) GetVersion() string {
|
||||||
|
return c.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommit returns debug flag
|
||||||
|
func (c *AppConfig) GetCommit() string {
|
||||||
|
return c.Commit
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuildDate returns debug flag
|
||||||
|
func (c *AppConfig) GetBuildDate() string {
|
||||||
|
return c.BuildDate
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns debug flag
|
||||||
|
func (c *AppConfig) GetName() string {
|
||||||
|
return c.Name
|
||||||
|
}
|
@ -1,9 +1,14 @@
|
|||||||
package main
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,53 +20,64 @@ import (
|
|||||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||||
// along the way
|
// along the way
|
||||||
|
|
||||||
type branchListBuilder struct{}
|
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||||
|
type BranchListBuilder struct {
|
||||||
func newBranchListBuilder() *branchListBuilder {
|
Log *logrus.Logger
|
||||||
return &branchListBuilder{}
|
GitCommand *commands.GitCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) obtainCurrentBranch() Branch {
|
// NewBranchListBuilder builds a new branch list builder
|
||||||
|
func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
|
||||||
|
return &BranchListBuilder{
|
||||||
|
Log: log,
|
||||||
|
GitCommand: gitCommand,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
|
||||||
// I used go-git for this, but that breaks if you've just done a git init,
|
// I used go-git for this, but that breaks if you've just done a git init,
|
||||||
// even though you're on 'master'
|
// even though you're on 'master'
|
||||||
branchName, _ := runDirectCommand("git symbolic-ref --short HEAD")
|
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||||
return Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*branchListBuilder) obtainReflogBranches() []Branch {
|
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
|
||||||
branches := make([]Branch, 0)
|
branches := make([]commands.Branch, 0)
|
||||||
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
branchLines := splitLines(rawString)
|
branchLines := utils.SplitLines(rawString)
|
||||||
for _, line := range branchLines {
|
for _, line := range branchLines {
|
||||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||||
branch := Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||||
branches = append(branches, branch)
|
branches = append(branches, branch)
|
||||||
}
|
}
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) obtainSafeBranches() []Branch {
|
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
|
||||||
branches := make([]Branch, 0)
|
branches := make([]commands.Branch, 0)
|
||||||
|
|
||||||
bIter, err := r.Branches()
|
bIter, err := b.GitCommand.Repo.Branches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
||||||
name := b.Name().Short()
|
name := b.Name().Short()
|
||||||
branches = append(branches, Branch{Name: name})
|
branches = append(branches, commands.Branch{Name: name})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch {
|
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
|
||||||
for _, newBranch := range newBranches {
|
for _, newBranch := range newBranches {
|
||||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||||
finalBranches = append(finalBranches, newBranch)
|
finalBranches = append(finalBranches, newBranch)
|
||||||
@ -70,7 +86,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi
|
|||||||
return finalBranches
|
return finalBranches
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
|
||||||
for _, safeBranch := range safeBranches {
|
for _, safeBranch := range safeBranches {
|
||||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||||
return safeBranch.Name
|
return safeBranch.Name
|
||||||
@ -79,15 +95,16 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
|||||||
return reflogBranch.Name
|
return reflogBranch.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) build() []Branch {
|
// Build the list of branches for the current repo
|
||||||
branches := make([]Branch, 0)
|
func (b *BranchListBuilder) Build() []commands.Branch {
|
||||||
|
branches := make([]commands.Branch, 0)
|
||||||
head := b.obtainCurrentBranch()
|
head := b.obtainCurrentBranch()
|
||||||
safeBranches := b.obtainSafeBranches()
|
safeBranches := b.obtainSafeBranches()
|
||||||
if len(safeBranches) == 0 {
|
if len(safeBranches) == 0 {
|
||||||
return append(branches, head)
|
return append(branches, head)
|
||||||
}
|
}
|
||||||
reflogBranches := b.obtainReflogBranches()
|
reflogBranches := b.obtainReflogBranches()
|
||||||
reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...))
|
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
|
||||||
for i, reflogBranch := range reflogBranches {
|
for i, reflogBranch := range reflogBranches {
|
||||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||||
}
|
}
|
||||||
@ -98,8 +115,17 @@ func (b *branchListBuilder) build() []Branch {
|
|||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueByName(branches []Branch) []Branch {
|
func branchIncluded(branchName string, branches []commands.Branch) bool {
|
||||||
finalBranches := make([]Branch, 0)
|
for _, existingBranch := range branches {
|
||||||
|
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueByName(branches []commands.Branch) []commands.Branch {
|
||||||
|
finalBranches := make([]commands.Branch, 0)
|
||||||
for _, branch := range branches {
|
for _, branch := range branches {
|
||||||
if branchIncluded(branch.Name, finalBranches) {
|
if branchIncluded(branch.Name, finalBranches) {
|
||||||
continue
|
continue
|
141
pkg/gui/branches_panel.go
Normal file
141
pkg/gui/branches_panel.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
index := gui.getItemPosition(v)
|
||||||
|
if index == 0 {
|
||||||
|
return gui.createErrorPanel(g, "You have already checked out this branch")
|
||||||
|
}
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
return gui.createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
branch := gui.State.Branches[0]
|
||||||
|
gui.createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
gui.refreshSidePanels(g)
|
||||||
|
return gui.handleBranchSelect(g, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
checkedOutBranch := gui.State.Branches[0]
|
||||||
|
selectedBranch := gui.getSelectedBranch(v)
|
||||||
|
if checkedOutBranch.Name == selectedBranch.Name {
|
||||||
|
return gui.createErrorPanel(g, "You cannot delete the checked out branch!")
|
||||||
|
}
|
||||||
|
return gui.createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
checkedOutBranch := gui.State.Branches[0]
|
||||||
|
selectedBranch := gui.getSelectedBranch(v)
|
||||||
|
defer gui.refreshSidePanels(g)
|
||||||
|
if checkedOutBranch.Name == selectedBranch.Name {
|
||||||
|
return gui.createErrorPanel(g, "You cannot merge a branch into itself")
|
||||||
|
}
|
||||||
|
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
|
||||||
|
lineNumber := gui.getItemPosition(v)
|
||||||
|
return gui.State.Branches[lineNumber]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"space": "checkout",
|
||||||
|
"f": "force checkout",
|
||||||
|
"m": "merge",
|
||||||
|
"c": "checkout by name",
|
||||||
|
"n": "new branch",
|
||||||
|
"d": "delete branch",
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// may want to standardise how these select methods work
|
||||||
|
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.renderBranchesOptions(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// This really shouldn't happen: there should always be a master branch
|
||||||
|
if len(gui.State.Branches) == 0 {
|
||||||
|
return gui.renderString(g, "main", "No branches for this repo")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
|
||||||
|
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
||||||
|
diff = "There is no tracking for this branch"
|
||||||
|
}
|
||||||
|
gui.renderString(g, "main", diff)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gui.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
|
||||||
|
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View("branches")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.Branches = builder.Build()
|
||||||
|
v.Clear()
|
||||||
|
for _, branch := range gui.State.Branches {
|
||||||
|
fmt.Fprintln(v, branch.GetDisplayString())
|
||||||
|
}
|
||||||
|
gui.resetOrigin(v)
|
||||||
|
return gui.refreshStatus(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
50
pkg/gui/commit_message_panel.go
Normal file
50
pkg/gui/commit_message_panel.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/gocui"
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
message := gui.trimmedContent(v)
|
||||||
|
if message == "" {
|
||||||
|
return gui.createErrorPanel(g, "You cannot commit without a commit message")
|
||||||
|
}
|
||||||
|
sub, err := gui.GitCommand.Commit(g, message)
|
||||||
|
if err != nil {
|
||||||
|
// TODO need to find a way to send through this error
|
||||||
|
if err != ErrSubProcess {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sub != nil {
|
||||||
|
gui.SubProcess = sub
|
||||||
|
return ErrSubProcess
|
||||||
|
}
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
v.Clear()
|
||||||
|
v.SetCursor(0, 0)
|
||||||
|
g.SetViewOnBottom("commitMessage")
|
||||||
|
gui.switchFocus(g, v, gui.getFilesView(g))
|
||||||
|
return gui.refreshCommits(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
g.SetViewOnBottom("commitMessage")
|
||||||
|
return gui.switchFocus(g, v, gui.getFilesView(g))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
// resising ahead of time so that the top line doesn't get hidden to make
|
||||||
|
// room for the cursor on the second line
|
||||||
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
|
||||||
|
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
|
||||||
|
if err != gocui.ErrUnknownView {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.EditNewLine()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.renderString(g, "options", "esc: close, enter: confirm")
|
||||||
|
}
|
176
pkg/gui/commits_panel.go
Normal file
176
pkg/gui/commits_panel.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoCommits : When no commits are found for the branch
|
||||||
|
ErrNoCommits = errors.New("No commits for this branch")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||||
|
g.Update(func(*gocui.Gui) error {
|
||||||
|
gui.State.Commits = gui.GitCommand.GetCommits()
|
||||||
|
v, err := g.View("commits")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v.Clear()
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
yellow := color.New(color.FgYellow)
|
||||||
|
white := color.New(color.FgWhite)
|
||||||
|
shaColor := white
|
||||||
|
for _, commit := range gui.State.Commits {
|
||||||
|
if commit.Pushed {
|
||||||
|
shaColor = red
|
||||||
|
} else {
|
||||||
|
shaColor = yellow
|
||||||
|
}
|
||||||
|
shaColor.Fprint(v, commit.Sha+" ")
|
||||||
|
white.Fprintln(v, commit.Name)
|
||||||
|
}
|
||||||
|
gui.refreshStatus(g)
|
||||||
|
if g.CurrentView().Name() == "commits" {
|
||||||
|
gui.handleCommitSelect(g, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||||
|
return gui.createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := gui.refreshFiles(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.resetOrigin(commitView)
|
||||||
|
return gui.handleCommitSelect(g, nil)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"s": "squash down",
|
||||||
|
"r": "rename",
|
||||||
|
"g": "reset to this commit",
|
||||||
|
"f": "fixup commit",
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.renderCommitsOptions(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != ErrNoCommits {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.renderString(g, "main", "No commits for this branch")
|
||||||
|
}
|
||||||
|
commitText := gui.GitCommand.Show(commit.Sha)
|
||||||
|
return gui.renderString(g, "main", commitText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.getItemPosition(v) != 0 {
|
||||||
|
return gui.createErrorPanel(g, "Can only squash topmost commit")
|
||||||
|
}
|
||||||
|
if len(gui.State.Commits) == 1 {
|
||||||
|
return gui.createErrorPanel(g, "You have no commits to squash with")
|
||||||
|
}
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.refreshStatus(g)
|
||||||
|
return gui.handleCommitSelect(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to files panel
|
||||||
|
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Tracked && file.HasUnstagedChanges {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if len(gui.State.Commits) == 1 {
|
||||||
|
return gui.createErrorPanel(g, "You have no commits to squash with")
|
||||||
|
}
|
||||||
|
if gui.anyUnStagedChanges(gui.State.Files) {
|
||||||
|
return gui.createErrorPanel(g, "Can't fixup while there are unstaged changes")
|
||||||
|
}
|
||||||
|
branch := gui.State.Branches[0]
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.refreshStatus(g)
|
||||||
|
}, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.getItemPosition(v) != 0 {
|
||||||
|
return gui.createErrorPanel(g, "Can only rename topmost commit")
|
||||||
|
}
|
||||||
|
gui.createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.handleCommitSelect(g, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
|
||||||
|
v, err := g.View("commits")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if len(gui.State.Commits) == 0 {
|
||||||
|
return commands.Commit{}, ErrNoCommits
|
||||||
|
}
|
||||||
|
lineNumber := gui.getItemPosition(v)
|
||||||
|
if lineNumber > len(gui.State.Commits)-1 {
|
||||||
|
gui.Log.Info("potential error in getSelected Commit (mismatched ui and state)", gui.State.Commits, lineNumber)
|
||||||
|
return gui.State.Commits[len(gui.State.Commits)-1], nil
|
||||||
|
}
|
||||||
|
return gui.State.Commits[lineNumber], nil
|
||||||
|
}
|
@ -4,39 +4,40 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package main
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||||
return func(g *gocui.Gui, v *gocui.View) error {
|
return func(g *gocui.Gui, v *gocui.View) error {
|
||||||
if function != nil {
|
if function != nil {
|
||||||
if err := function(g, v); err != nil {
|
if err := function(g, v); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return closeConfirmationPrompt(g)
|
return gui.closeConfirmationPrompt(g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeConfirmationPrompt(g *gocui.Gui) error {
|
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
|
||||||
view, err := g.View("confirmation")
|
view, err := g.View("confirmation")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := returnFocus(g, view); err != nil {
|
if err := gui.returnFocus(g, view); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
g.DeleteKeybindings("confirmation")
|
g.DeleteKeybindings("confirmation")
|
||||||
return g.DeleteView("confirmation")
|
return g.DeleteView("confirmation")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMessageHeight(message string, width int) int {
|
func (gui *Gui) getMessageHeight(message string, width int) int {
|
||||||
lines := strings.Split(message, "\n")
|
lines := strings.Split(message, "\n")
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
@ -45,20 +46,20 @@ func getMessageHeight(message string, width int) int {
|
|||||||
return lineCount
|
return lineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
panelWidth := width / 2
|
panelWidth := width / 2
|
||||||
panelHeight := getMessageHeight(prompt, panelWidth)
|
panelHeight := gui.getMessageHeight(prompt, panelWidth)
|
||||||
return width/2 - panelWidth/2,
|
return width/2 - panelWidth/2,
|
||||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||||
width/2 + panelWidth/2,
|
width/2 + panelWidth/2,
|
||||||
height/2 + panelHeight/2
|
height/2 + panelHeight/2
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||||
g.SetViewOnBottom("commitMessage")
|
g.SetViewOnBottom("commitMessage")
|
||||||
// only need to fit one line
|
// only need to fit one line
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "")
|
||||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
@ -66,41 +67,41 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand
|
|||||||
|
|
||||||
confirmationView.Editable = true
|
confirmationView.Editable = true
|
||||||
confirmationView.Title = title
|
confirmationView.Title = title
|
||||||
switchFocus(g, currentView, confirmationView)
|
gui.switchFocus(g, currentView, confirmationView)
|
||||||
return setKeyBindings(g, handleConfirm, nil)
|
return gui.setKeyBindings(g, handleConfirm, nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
g.SetViewOnBottom("commitMessage")
|
g.SetViewOnBottom("commitMessage")
|
||||||
g.Update(func(g *gocui.Gui) error {
|
g.Update(func(g *gocui.Gui) error {
|
||||||
// delete the existing confirmation panel if it exists
|
// delete the existing confirmation panel if it exists
|
||||||
if view, _ := g.View("confirmation"); view != nil {
|
if view, _ := g.View("confirmation"); view != nil {
|
||||||
if err := closeConfirmationPrompt(g); err != nil {
|
if err := gui.closeConfirmationPrompt(g); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt)
|
||||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
confirmationView.Title = title
|
confirmationView.Title = title
|
||||||
confirmationView.FgColor = gocui.ColorWhite
|
confirmationView.FgColor = gocui.ColorWhite
|
||||||
renderString(g, "confirmation", prompt)
|
gui.renderString(g, "confirmation", prompt)
|
||||||
switchFocus(g, currentView, confirmationView)
|
gui.switchFocus(g, currentView, confirmationView)
|
||||||
return setKeyBindings(g, handleConfirm, handleClose)
|
return gui.setKeyBindings(g, handleConfirm, handleClose)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNewline(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error {
|
||||||
// resising ahead of time so that the top line doesn't get hidden to make
|
// resising ahead of time so that the top line doesn't get hidden to make
|
||||||
// room for the cursor on the second line
|
// room for the cursor on the second line
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
|
||||||
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
|
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
@ -111,45 +112,38 @@ func handleNewline(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
renderString(g, "options", "esc: close, enter: confirm")
|
gui.renderString(g, "options", "esc: close, enter: confirm")
|
||||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil {
|
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, handleNewline); err != nil {
|
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, gui.handleNewline); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleClose))
|
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
||||||
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createErrorPanel(g *gocui.Gui, message string) error {
|
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||||
currentView := g.CurrentView()
|
currentView := g.CurrentView()
|
||||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||||
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
|
return gui.createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimTrailingNewline(str string) string {
|
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
||||||
if strings.HasSuffix(str, "\n") {
|
|
||||||
return str[:len(str)-1]
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
func resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
// If the confirmation panel is already displayed, just resize the width,
|
// If the confirmation panel is already displayed, just resize the width,
|
||||||
// otherwise continue
|
// otherwise continue
|
||||||
content := trimTrailingNewline(v.Buffer())
|
content := utils.TrimTrailingNewline(v.Buffer())
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content)
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
|
||||||
vx0, vy0, vx1, vy1 := v.Dimensions()
|
vx0, vy0, vx1, vy1 := v.Dimensions()
|
||||||
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
devLog("resizing popup panel")
|
gui.Log.Info("resizing popup panel")
|
||||||
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
400
pkg/gui/files_panel.go
Normal file
400
pkg/gui/files_panel.go
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
// "io"
|
||||||
|
// "io/ioutil"
|
||||||
|
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoFiles = errors.New("No changed files")
|
||||||
|
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) stagedFiles() []commands.File {
|
||||||
|
files := gui.State.Files
|
||||||
|
result := make([]commands.File, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.HasStagedChanges {
|
||||||
|
result = append(result, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) trackedFiles() []commands.File {
|
||||||
|
files := gui.State.Files
|
||||||
|
result := make([]commands.File, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Tracked {
|
||||||
|
result = append(result, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.GitCommand.StageFile(file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.HasMergeConflicts {
|
||||||
|
return gui.handleSwitchToMerge(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.HasUnstagedChanges {
|
||||||
|
gui.GitCommand.StageFile(file.Name)
|
||||||
|
} else {
|
||||||
|
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gui.refreshFiles(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.handleFileSelect(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !file.HasUnstagedChanges {
|
||||||
|
return gui.createErrorPanel(g, "File has no unstaged changes to add")
|
||||||
|
}
|
||||||
|
if !file.Tracked {
|
||||||
|
return gui.createErrorPanel(g, "Cannot git add --patch untracked files")
|
||||||
|
}
|
||||||
|
sub, err := gui.GitCommand.AddPatch(file.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.SubProcess = sub
|
||||||
|
return ErrSubProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
|
||||||
|
if len(gui.State.Files) == 0 {
|
||||||
|
return commands.File{}, errNoFiles
|
||||||
|
}
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lineNumber := gui.getItemPosition(filesView)
|
||||||
|
return gui.State.Files[lineNumber], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var deleteVerb string
|
||||||
|
if file.Tracked {
|
||||||
|
deleteVerb = "checkout"
|
||||||
|
} else {
|
||||||
|
deleteVerb = "delete"
|
||||||
|
}
|
||||||
|
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.RemoveFile(file); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if file.Tracked {
|
||||||
|
return gui.createErrorPanel(g, "Cannot ignore tracked files")
|
||||||
|
}
|
||||||
|
gui.GitCommand.Ignore(file.Name)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
|
||||||
|
optionsMap := map[string]string{
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
"S": "stash files",
|
||||||
|
"c": "commit changes",
|
||||||
|
"o": "open",
|
||||||
|
"i": "ignore",
|
||||||
|
"d": "delete",
|
||||||
|
"space": "toggle staged",
|
||||||
|
"R": "refresh",
|
||||||
|
"t": "add patch",
|
||||||
|
"e": "edit",
|
||||||
|
"PgUp/PgDn": "scroll",
|
||||||
|
}
|
||||||
|
if gui.State.HasMergeConflicts {
|
||||||
|
optionsMap["a"] = "abort merge"
|
||||||
|
optionsMap["m"] = "resolve merge conflicts"
|
||||||
|
}
|
||||||
|
if file == nil {
|
||||||
|
return gui.renderOptionsMap(g, optionsMap)
|
||||||
|
}
|
||||||
|
if file.Tracked {
|
||||||
|
optionsMap["d"] = "checkout"
|
||||||
|
}
|
||||||
|
return gui.renderOptionsMap(g, optionsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.renderString(g, "main", "No changed files")
|
||||||
|
return gui.renderfilesOptions(g, nil)
|
||||||
|
}
|
||||||
|
gui.renderfilesOptions(g, &file)
|
||||||
|
var content string
|
||||||
|
if file.HasMergeConflicts {
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
content = gui.GitCommand.Diff(file)
|
||||||
|
return gui.renderString(g, "main", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "There are no staged files to commit")
|
||||||
|
}
|
||||||
|
commitMessageView := gui.getCommitMessageView(g)
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
g.SetViewOnTop("commitMessage")
|
||||||
|
gui.switchFocus(g, filesView, commitMessageView)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||||
|
// their editor rather than via the popup panel
|
||||||
|
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "There are no staged files to commit")
|
||||||
|
}
|
||||||
|
gui.PrepareSubProcess(g, "git", "commit")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
|
||||||
|
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error {
|
||||||
|
sub, err := gui.GitCommand.PrepareCommitSubProcess()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.SubProcess = sub
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
return ErrSubProcess
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (*exec.Cmd, error)) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sub, err := open(file.Name)
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if sub != nil {
|
||||||
|
gui.SubProcess = sub
|
||||||
|
return ErrSubProcess
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.OSCommand.EditFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.OSCommand.OpenFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.OSCommand.SublimeOpenFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.OSCommand.VsCodeOpenFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshStateFiles() {
|
||||||
|
// get files to stage
|
||||||
|
files := gui.GitCommand.GetStatusFiles()
|
||||||
|
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
|
||||||
|
gui.updateHasMergeConflictStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) updateHasMergeConflictStatus() error {
|
||||||
|
merging, err := gui.GitCommand.IsInMergeState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.HasMergeConflicts = merging
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
|
||||||
|
// potentially inefficient to be instantiating these color
|
||||||
|
// objects with each render
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
green := color.New(color.FgGreen)
|
||||||
|
if !file.Tracked && !file.HasStagedChanges {
|
||||||
|
red.Fprintln(filesView, file.DisplayString)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
green.Fprint(filesView, file.DisplayString[0:1])
|
||||||
|
red.Fprint(filesView, file.DisplayString[1:3])
|
||||||
|
if file.HasUnstagedChanges {
|
||||||
|
red.Fprintln(filesView, file.Name)
|
||||||
|
} else {
|
||||||
|
green.Fprintln(filesView, file.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||||
|
item, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", gui.renderString(g, "main", "No file to display")
|
||||||
|
}
|
||||||
|
cat, err := gui.GitCommand.CatFile(item.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return cat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.refreshStateFiles()
|
||||||
|
filesView.Clear()
|
||||||
|
for _, file := range gui.State.Files {
|
||||||
|
gui.renderFile(file, filesView)
|
||||||
|
}
|
||||||
|
gui.correctCursor(filesView)
|
||||||
|
if filesView == g.CurrentView() {
|
||||||
|
gui.handleFileSelect(g, filesView)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.createMessagePanel(g, v, "", "Pulling...")
|
||||||
|
go func() {
|
||||||
|
if err := gui.GitCommand.Pull(); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
} else {
|
||||||
|
gui.closeConfirmationPrompt(g)
|
||||||
|
gui.refreshCommits(g)
|
||||||
|
gui.refreshStatus(g)
|
||||||
|
}
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.createMessagePanel(g, v, "", "Pushing...")
|
||||||
|
go func() {
|
||||||
|
branchName := gui.State.Branches[0].Name
|
||||||
|
if err := gui.GitCommand.Push(branchName); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
} else {
|
||||||
|
gui.closeConfirmationPrompt(g)
|
||||||
|
gui.refreshCommits(g)
|
||||||
|
gui.refreshStatus(g)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
mergeView, err := g.View("main")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !file.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "This file has no merge conflicts")
|
||||||
|
}
|
||||||
|
gui.switchFocus(g, v, mergeView)
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.AbortMerge(); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
gui.createMessagePanel(g, v, "", "Merge aborted")
|
||||||
|
gui.refreshStatus(g)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.ResetHard(); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
@ -1,82 +1,86 @@
|
|||||||
package main
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
||||||
// "io"
|
// "io"
|
||||||
// "io/ioutil"
|
// "io/ioutil"
|
||||||
|
|
||||||
"runtime"
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// "strings"
|
// "strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/golang-collections/collections/stack"
|
"github.com/golang-collections/collections/stack"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OverlappingEdges determines if panel edges overlap
|
// OverlappingEdges determines if panel edges overlap
|
||||||
var OverlappingEdges = false
|
var OverlappingEdges = false
|
||||||
|
|
||||||
type stateType struct {
|
// ErrSubProcess tells us we're switching to a subprocess so we need to
|
||||||
GitFiles []GitFile
|
// close the Gui until it is finished
|
||||||
Branches []Branch
|
var (
|
||||||
Commits []Commit
|
ErrSubProcess = errors.New("running subprocess")
|
||||||
StashEntries []StashEntry
|
)
|
||||||
|
|
||||||
|
// Gui wraps the gocui Gui object which handles rendering and events
|
||||||
|
type Gui struct {
|
||||||
|
g *gocui.Gui
|
||||||
|
Log *logrus.Logger
|
||||||
|
GitCommand *commands.GitCommand
|
||||||
|
OSCommand *commands.OSCommand
|
||||||
|
Version string
|
||||||
|
SubProcess *exec.Cmd
|
||||||
|
State guiState
|
||||||
|
}
|
||||||
|
|
||||||
|
type guiState struct {
|
||||||
|
Files []commands.File
|
||||||
|
Branches []commands.Branch
|
||||||
|
Commits []commands.Commit
|
||||||
|
StashEntries []commands.StashEntry
|
||||||
PreviousView string
|
PreviousView string
|
||||||
HasMergeConflicts bool
|
HasMergeConflicts bool
|
||||||
ConflictIndex int
|
ConflictIndex int
|
||||||
ConflictTop bool
|
ConflictTop bool
|
||||||
Conflicts []conflict
|
Conflicts []commands.Conflict
|
||||||
EditHistory *stack.Stack
|
EditHistory *stack.Stack
|
||||||
Platform platform
|
Platform commands.Platform
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
type conflict struct {
|
// NewGui builds a new gui handler
|
||||||
start int
|
func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, version string) (*Gui, error) {
|
||||||
middle int
|
initialState := guiState{
|
||||||
end int
|
Files: make([]commands.File, 0),
|
||||||
}
|
PreviousView: "files",
|
||||||
|
Commits: make([]commands.Commit, 0),
|
||||||
var state = stateType{
|
StashEntries: make([]commands.StashEntry, 0),
|
||||||
GitFiles: make([]GitFile, 0),
|
ConflictIndex: 0,
|
||||||
PreviousView: "files",
|
ConflictTop: true,
|
||||||
Commits: make([]Commit, 0),
|
Conflicts: make([]commands.Conflict, 0),
|
||||||
StashEntries: make([]StashEntry, 0),
|
EditHistory: stack.New(),
|
||||||
ConflictIndex: 0,
|
Platform: *oSCommand.Platform,
|
||||||
ConflictTop: true,
|
Version: "test version", // TODO: send version in
|
||||||
Conflicts: make([]conflict, 0),
|
|
||||||
EditHistory: stack.New(),
|
|
||||||
Platform: getPlatform(),
|
|
||||||
}
|
|
||||||
|
|
||||||
type platform struct {
|
|
||||||
os string
|
|
||||||
shell string
|
|
||||||
shellArg string
|
|
||||||
escapedQuote string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPlatform() platform {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
return platform{
|
|
||||||
os: "windows",
|
|
||||||
shell: "cmd",
|
|
||||||
shellArg: "/c",
|
|
||||||
escapedQuote: "\\\"",
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return platform{
|
|
||||||
os: runtime.GOOS,
|
|
||||||
shell: "bash",
|
|
||||||
shellArg: "-c",
|
|
||||||
escapedQuote: "\"",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &Gui{
|
||||||
|
Log: log,
|
||||||
|
GitCommand: gitCommand,
|
||||||
|
OSCommand: oSCommand,
|
||||||
|
Version: version,
|
||||||
|
State: initialState,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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 {
|
||||||
@ -85,7 +89,7 @@ func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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()) {
|
||||||
@ -94,8 +98,8 @@ func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||||
return refreshSidePanels(g)
|
return gui.refreshSidePanels(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
func max(a, b int) int {
|
||||||
@ -106,7 +110,7 @@ func max(a, b int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// layout is called for every screen re-render e.g. when the screen is resized
|
// layout is called for every screen re-render e.g. when the screen is resized
|
||||||
func layout(g *gocui.Gui) error {
|
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||||
g.Highlight = true
|
g.Highlight = true
|
||||||
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
|
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
@ -157,7 +161,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = ShortLocalize("StatusTitle", "Status")
|
v.Title = "Status"
|
||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +171,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filesView.Highlight = true
|
filesView.Highlight = true
|
||||||
filesView.Title = ShortLocalize("FilesTitle", "Files")
|
filesView.Title = "Files"
|
||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +179,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = ShortLocalize("BranchesTitle", "Branches")
|
v.Title = "Branches"
|
||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +187,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = ShortLocalize("CommitsTitle", "Commits")
|
v.Title = "Commits"
|
||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,11 +195,11 @@ func layout(g *gocui.Gui) error {
|
|||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.Title = ShortLocalize("StashTitle", "Stash")
|
v.Title = "Stash"
|
||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
|
if v, err := g.SetView("options", -1, optionsTop, width-len(gui.Version)-2, optionsTop+2, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -203,60 +207,60 @@ func layout(g *gocui.Gui) error {
|
|||||||
v.Frame = false
|
v.Frame = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if getCommitMessageView(g) == nil {
|
if gui.getCommitMessageView(g) == nil {
|
||||||
// doesn't matter where this view starts because it will be hidden
|
// doesn't matter where this view starts because it will be hidden
|
||||||
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
|
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
g.SetViewOnBottom("commitMessage")
|
g.SetViewOnBottom("commitMessage")
|
||||||
commitMessageView.Title = ShortLocalize("CommitMessage", "Commit message")
|
commitMessageView.Title = "Commit message"
|
||||||
commitMessageView.FgColor = gocui.ColorWhite
|
commitMessageView.FgColor = gocui.ColorWhite
|
||||||
commitMessageView.Editable = true
|
commitMessageView.Editable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
if v, err := g.SetView("version", width-len(gui.Version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.BgColor = gocui.ColorDefault
|
v.BgColor = gocui.ColorDefault
|
||||||
v.FgColor = gocui.ColorGreen
|
v.FgColor = gocui.ColorGreen
|
||||||
v.Frame = false
|
v.Frame = false
|
||||||
renderString(g, "version", version)
|
gui.renderString(g, "version", gui.Version)
|
||||||
|
|
||||||
// these are only called once
|
// these are only called once
|
||||||
handleFileSelect(g, filesView)
|
gui.handleFileSelect(g, filesView)
|
||||||
refreshFiles(g)
|
gui.refreshFiles(g)
|
||||||
refreshBranches(g)
|
gui.refreshBranches(g)
|
||||||
refreshCommits(g)
|
gui.refreshCommits(g)
|
||||||
refreshStashEntries(g)
|
gui.refreshStashEntries(g)
|
||||||
nextView(g, nil)
|
gui.nextView(g, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
resizePopupPanels(g)
|
gui.resizePopupPanels(g)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch(g *gocui.Gui) error {
|
func (gui *Gui) fetch(g *gocui.Gui) error {
|
||||||
gitFetch()
|
gui.GitCommand.Fetch()
|
||||||
refreshStatus(g)
|
gui.refreshStatus(g)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLoader(g *gocui.Gui) error {
|
func (gui *Gui) updateLoader(g *gocui.Gui) error {
|
||||||
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
||||||
content := trimmedContent(confirmationView)
|
content := gui.trimmedContent(confirmationView)
|
||||||
if strings.Contains(content, "...") {
|
if strings.Contains(content, "...") {
|
||||||
staticContent := strings.Split(content, "...")[0] + "..."
|
staticContent := strings.Split(content, "...")[0] + "..."
|
||||||
renderString(g, "confirmation", staticContent+" "+loader())
|
gui.renderString(g, "confirmation", staticContent+" "+gui.loader())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
|
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
|
||||||
go func() {
|
go func() {
|
||||||
for range time.Tick(interval) {
|
for range time.Tick(interval) {
|
||||||
function(g)
|
function(g)
|
||||||
@ -264,37 +268,64 @@ func goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) err
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizePopupPanels(g *gocui.Gui) error {
|
func (gui *Gui) resizePopupPanels(g *gocui.Gui) error {
|
||||||
v := g.CurrentView()
|
v := g.CurrentView()
|
||||||
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
|
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
|
||||||
return resizePopupPanel(g, v)
|
return gui.resizePopupPanel(g, v)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() (err error) {
|
// Run setup the gui with keybindings and start the mainloop
|
||||||
|
func (gui *Gui) Run() error {
|
||||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer g.Close()
|
defer g.Close()
|
||||||
|
|
||||||
|
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||||
|
|
||||||
g.FgColor = gocui.ColorDefault
|
g.FgColor = gocui.ColorDefault
|
||||||
|
|
||||||
goEvery(g, time.Second*60, fetch)
|
gui.goEvery(g, time.Second*60, gui.fetch)
|
||||||
goEvery(g, time.Second*10, refreshFiles)
|
gui.goEvery(g, time.Second*10, gui.refreshFiles)
|
||||||
goEvery(g, time.Millisecond*10, updateLoader)
|
gui.goEvery(g, time.Millisecond*10, gui.updateLoader)
|
||||||
|
|
||||||
g.SetManagerFunc(layout)
|
g.SetManagerFunc(gui.layout)
|
||||||
|
|
||||||
if err = keybindings(g); err != nil {
|
if err = gui.keybindings(g); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = g.MainLoop()
|
err = g.MainLoop()
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
|
||||||
|
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
|
||||||
|
// otherwise it handles the error, possibly by quitting the application
|
||||||
|
func (gui *Gui) RunWithSubprocesses() {
|
||||||
|
for {
|
||||||
|
if err := gui.Run(); err != nil {
|
||||||
|
if err == gocui.ErrQuit {
|
||||||
|
break
|
||||||
|
} else if err == ErrSubProcess {
|
||||||
|
gui.SubProcess.Stdin = os.Stdin
|
||||||
|
gui.SubProcess.Stdout = os.Stdout
|
||||||
|
gui.SubProcess.Stderr = os.Stderr
|
||||||
|
gui.SubProcess.Run()
|
||||||
|
gui.SubProcess.Stdout = ioutil.Discard
|
||||||
|
gui.SubProcess.Stderr = ioutil.Discard
|
||||||
|
gui.SubProcess.Stdin = nil
|
||||||
|
gui.SubProcess = nil
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package gui
|
||||||
|
|
||||||
import "github.com/jesseduffield/gocui"
|
import "github.com/jesseduffield/gocui"
|
||||||
|
|
||||||
@ -12,73 +12,75 @@ type Binding struct {
|
|||||||
Modifier gocui.Modifier
|
Modifier gocui.Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func keybindings(g *gocui.Gui) error {
|
func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||||
bindings := []Binding{
|
bindings := []Binding{
|
||||||
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
|
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit},
|
||||||
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
|
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit},
|
||||||
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
|
||||||
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
|
||||||
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
|
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
|
||||||
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
|
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
|
||||||
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
|
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles},
|
||||||
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
|
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles},
|
||||||
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress},
|
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
|
||||||
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
|
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress},
|
||||||
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
|
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
|
||||||
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
|
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress},
|
||||||
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
|
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove},
|
||||||
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
|
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge},
|
||||||
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
|
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit},
|
||||||
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
|
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen},
|
||||||
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
|
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen},
|
||||||
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
|
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen},
|
||||||
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
|
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile},
|
||||||
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
|
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles},
|
||||||
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
|
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave},
|
||||||
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
|
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge},
|
||||||
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
|
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch},
|
||||||
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
|
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard},
|
||||||
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
|
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
|
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
|
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
|
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot},
|
||||||
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
|
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress},
|
||||||
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
|
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName},
|
||||||
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch},
|
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
|
||||||
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
|
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
|
||||||
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
|
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
|
||||||
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
|
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
|
||||||
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
|
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
|
||||||
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
|
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},
|
||||||
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
|
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit},
|
||||||
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
|
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup},
|
||||||
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
|
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm},
|
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose},
|
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage},
|
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm},
|
||||||
|
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose},
|
||||||
|
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Would make these keybindings global but that interferes with editing
|
// Would make these keybindings global but that interferes with editing
|
||||||
// input in the confirmation panel
|
// input in the confirmation panel
|
||||||
for _, viewName := range []string{"files", "branches", "commits", "stash"} {
|
for _, viewName := range []string{"files", "branches", "commits", "stash"} {
|
||||||
bindings = append(bindings, []Binding{
|
bindings = append(bindings, []Binding{
|
||||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
|
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
|
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
|
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
|
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
|
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown},
|
||||||
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView},
|
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||||
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView},
|
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp},
|
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp},
|
||||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown},
|
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown},
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
|
|
260
pkg/gui/merge_panel.go
Normal file
260
pkg/gui/merge_panel.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
||||||
|
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
|
||||||
|
conflicts := make([]commands.Conflict, 0)
|
||||||
|
var newConflict commands.Conflict
|
||||||
|
for i, line := range utils.SplitLines(content) {
|
||||||
|
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
||||||
|
newConflict = commands.Conflict{Start: i}
|
||||||
|
} else if line == "=======" {
|
||||||
|
newConflict.Middle = i
|
||||||
|
} else if strings.HasPrefix(line, ">>>>>>> ") {
|
||||||
|
newConflict.End = i
|
||||||
|
conflicts = append(conflicts, newConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conflicts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||||
|
return conflicts[0], conflicts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||||
|
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
||||||
|
if len(conflicts) == 0 {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
conflict, remainingConflicts := gui.shiftConflict(conflicts)
|
||||||
|
var outputBuffer bytes.Buffer
|
||||||
|
for i, line := range utils.SplitLines(content) {
|
||||||
|
colourAttr := color.FgWhite
|
||||||
|
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||||
|
colourAttr = color.FgRed
|
||||||
|
}
|
||||||
|
colour := color.New(colourAttr)
|
||||||
|
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
|
||||||
|
colour.Add(color.Bold)
|
||||||
|
}
|
||||||
|
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||||
|
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
|
||||||
|
}
|
||||||
|
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||||
|
}
|
||||||
|
return outputBuffer.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.State.ConflictTop = true
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.State.ConflictTop = false
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gui.State.ConflictIndex++
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.State.ConflictIndex <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gui.State.ConflictIndex--
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
|
||||||
|
return i == conflict.Middle ||
|
||||||
|
i == conflict.Start ||
|
||||||
|
i == conflict.End ||
|
||||||
|
pick != "both" &&
|
||||||
|
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
|
||||||
|
(pick == "top" && i > conflict.Middle && i < conflict.End)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
|
||||||
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.Open(gitFile.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
output := ""
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !gui.isIndexToDelete(i, conflict, pick) {
|
||||||
|
output += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gui.Log.Info(output)
|
||||||
|
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
|
||||||
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.EditHistory.Push(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.State.EditHistory.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
prevContent := gui.State.EditHistory.Pop().(string)
|
||||||
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||||
|
gui.pushFileSnapshot(g)
|
||||||
|
pick := "bottom"
|
||||||
|
if gui.State.ConflictTop {
|
||||||
|
pick = "top"
|
||||||
|
}
|
||||||
|
err := gui.resolveConflict(g, conflict, pick)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.refreshMergePanel(g)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||||
|
gui.pushFileSnapshot(g)
|
||||||
|
err := gui.resolveConflict(g, conflict, "both")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
|
||||||
|
cat, err := gui.catSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.Conflicts, err = gui.findConflicts(cat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gui.State.Conflicts) == 0 {
|
||||||
|
return gui.handleCompleteMerge(g)
|
||||||
|
} else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
|
||||||
|
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
|
||||||
|
}
|
||||||
|
hasFocus := gui.currentViewName(g) == "main"
|
||||||
|
if hasFocus {
|
||||||
|
gui.renderMergeOptions(g)
|
||||||
|
}
|
||||||
|
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gui.scrollToConflict(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.renderString(g, "main", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
|
||||||
|
mainView, err := g.View("main")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(gui.State.Conflicts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||||
|
ox, _ := mainView.Origin()
|
||||||
|
_, height := mainView.Size()
|
||||||
|
conflictMiddle := (conflict.End + conflict.Start) / 2
|
||||||
|
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
||||||
|
return mainView.SetOrigin(ox, newOriginY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) switchToMerging(g *gocui.Gui) error {
|
||||||
|
gui.State.ConflictIndex = 0
|
||||||
|
gui.State.ConflictTop = true
|
||||||
|
_, err := g.SetCurrentView("main")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"↑ ↓": "select hunk",
|
||||||
|
"← →": "navigate conflicts",
|
||||||
|
"space": "pick hunk",
|
||||||
|
"b": "pick both hunks",
|
||||||
|
"z": "undo",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
return gui.switchFocus(g, v, filesView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.stageSelectedFile(g)
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
return gui.switchFocus(g, nil, filesView)
|
||||||
|
}
|
97
pkg/gui/stash_panel.go
Normal file
97
pkg/gui/stash_panel.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View("stash")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
|
||||||
|
v.Clear()
|
||||||
|
for _, stashEntry := range gui.State.StashEntries {
|
||||||
|
fmt.Fprintln(v, stashEntry.DisplayString)
|
||||||
|
}
|
||||||
|
return gui.resetOrigin(v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||||
|
if len(gui.State.StashEntries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lineNumber := gui.getItemPosition(v)
|
||||||
|
return &gui.State.StashEntries[lineNumber]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"space": "apply",
|
||||||
|
"g": "pop",
|
||||||
|
"d": "drop",
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.renderStashOptions(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
stashEntry := gui.getSelectedStashEntry(v)
|
||||||
|
if stashEntry == nil {
|
||||||
|
gui.renderString(g, "main", "No stash entries")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
|
||||||
|
gui.renderString(g, "main", diff)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.stashDo(g, v, "apply")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.stashDo(g, v, "pop")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.stashDo(g, v, "drop")
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||||
|
stashEntry := gui.getSelectedStashEntry(v)
|
||||||
|
if stashEntry == nil {
|
||||||
|
return gui.createErrorPanel(g, "No stash to "+method)
|
||||||
|
}
|
||||||
|
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
gui.refreshStashEntries(g)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
|
||||||
|
return gui.createErrorPanel(g, "You have no tracked/staged files to stash")
|
||||||
|
}
|
||||||
|
gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
gui.refreshStashEntries(g)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
package main
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func refreshStatus(g *gocui.Gui) error {
|
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||||
v, err := g.View("status")
|
v, err := g.View("status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -17,22 +18,22 @@ func refreshStatus(g *gocui.Gui) error {
|
|||||||
// contents end up cleared
|
// contents end up cleared
|
||||||
g.Update(func(*gocui.Gui) error {
|
g.Update(func(*gocui.Gui) error {
|
||||||
v.Clear()
|
v.Clear()
|
||||||
pushables, pullables := gitUpstreamDifferenceCount()
|
pushables, pullables := gui.GitCommand.UpstreamDifferenceCount()
|
||||||
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
||||||
branches := state.Branches
|
branches := gui.State.Branches
|
||||||
if err := updateHasMergeConflictStatus(); err != nil {
|
if err := gui.updateHasMergeConflictStatus(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if state.HasMergeConflicts {
|
if gui.State.HasMergeConflicts {
|
||||||
fmt.Fprint(v, coloredString(" (merging)", color.FgYellow))
|
fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(branches) == 0 {
|
if len(branches) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
branch := branches[0]
|
branch := branches[0]
|
||||||
name := coloredString(branch.Name, branch.getColor())
|
name := utils.ColoredString(branch.Name, branch.GetColor())
|
||||||
repo := getCurrentProject()
|
repo := utils.GetCurrentRepoName()
|
||||||
fmt.Fprint(v, " "+repo+" → "+name)
|
fmt.Fprint(v, " "+repo+" → "+name)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -11,14 +11,14 @@ import (
|
|||||||
|
|
||||||
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
||||||
|
|
||||||
func refreshSidePanels(g *gocui.Gui) error {
|
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||||
refreshBranches(g)
|
gui.refreshBranches(g)
|
||||||
refreshFiles(g)
|
gui.refreshFiles(g)
|
||||||
refreshCommits(g)
|
gui.refreshCommits(g)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextView(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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]
|
||||||
@ -29,7 +29,7 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i == len(cyclableViews)-1 {
|
if i == len(cyclableViews)-1 {
|
||||||
devLog(v.Name() + " is not in the list of views")
|
gui.Log.Info(v.Name() + " is not in the list of views")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,10 +38,10 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, focusedView)
|
return gui.switchFocus(g, v, focusedView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func previousView(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||||
var focusedViewName string
|
var focusedViewName string
|
||||||
if v == nil || v.Name() == cyclableViews[0] {
|
if v == nil || v.Name() == cyclableViews[0] {
|
||||||
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
||||||
@ -52,7 +52,7 @@ func previousView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i == len(cyclableViews)-1 {
|
if i == len(cyclableViews)-1 {
|
||||||
devLog(v.Name() + " is not in the list of views")
|
gui.Log.Info(v.Name() + " is not in the list of views")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,69 +61,70 @@ func previousView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, focusedView)
|
return gui.switchFocus(g, v, focusedView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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 gui.handleFileSelect(g, v)
|
||||||
case "branches":
|
case "branches":
|
||||||
return handleBranchSelect(g, v)
|
return gui.handleBranchSelect(g, v)
|
||||||
case "confirmation":
|
case "confirmation":
|
||||||
return nil
|
return nil
|
||||||
case "commitMessage":
|
case "commitMessage":
|
||||||
return handleCommitFocused(g, v)
|
return gui.handleCommitFocused(g, v)
|
||||||
case "main":
|
case "main":
|
||||||
// TODO: pull this out into a 'view focused' function
|
// TODO: pull this out into a 'view focused' function
|
||||||
refreshMergePanel(g)
|
gui.refreshMergePanel(g)
|
||||||
v.Highlight = false
|
v.Highlight = false
|
||||||
return nil
|
return nil
|
||||||
case "commits":
|
case "commits":
|
||||||
return handleCommitSelect(g, v)
|
return gui.handleCommitSelect(g, v)
|
||||||
case "stash":
|
case "stash":
|
||||||
return handleStashEntrySelect(g, v)
|
return gui.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 (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||||
previousView, err := g.View(state.PreviousView)
|
previousView, err := g.View(gui.State.PreviousView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, previousView)
|
return gui.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 (gui *Gui) 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())
|
gui.Log.Info("setting previous view to:", oldView.Name())
|
||||||
state.PreviousView = oldView.Name()
|
gui.State.PreviousView = oldView.Name()
|
||||||
}
|
}
|
||||||
newView.Highlight = true
|
newView.Highlight = true
|
||||||
devLog("new focused view is " + newView.Name())
|
gui.Log.Info("new focused view is " + 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 gui.newLineFocused(g, newView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemPosition(v *gocui.View) int {
|
func (gui *Gui) getItemPosition(v *gocui.View) int {
|
||||||
|
gui.correctCursor(v)
|
||||||
_, cy := v.Cursor()
|
_, cy := v.Cursor()
|
||||||
_, oy := v.Origin()
|
_, oy := v.Origin()
|
||||||
return oy + cy
|
return oy + cy
|
||||||
}
|
}
|
||||||
|
|
||||||
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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" {
|
||||||
@ -138,11 +139,11 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newLineFocused(g, v)
|
gui.newLineFocused(g, v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) 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" {
|
||||||
@ -159,19 +160,19 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newLineFocused(g, v)
|
gui.newLineFocused(g, v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetOrigin(v *gocui.View) error {
|
func (gui *Gui) 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 to the last line
|
||||||
func correctCursor(v *gocui.View) error {
|
func (gui *Gui) 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
|
||||||
@ -181,7 +182,7 @@ func correctCursor(v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderString(g *gocui.Gui, viewName, s string) error {
|
func (gui *Gui) 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)
|
||||||
// just in case the view disappeared as this function was called, we'll
|
// just in case the view disappeared as this function was called, we'll
|
||||||
@ -197,7 +198,7 @@ func renderString(g *gocui.Gui, viewName, s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func optionsMapToString(optionsMap map[string]string) string {
|
func (gui *Gui) 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)
|
||||||
@ -206,11 +207,11 @@ func optionsMapToString(optionsMap map[string]string) string {
|
|||||||
return strings.Join(optionsArray, ", ")
|
return strings.Join(optionsArray, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
|
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
|
||||||
return renderString(g, "options", optionsMapToString(optionsMap))
|
return gui.renderString(g, "options", gui.optionsMapToString(optionsMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loader() string {
|
func (gui *Gui) loader() string {
|
||||||
characters := "|/-\\"
|
characters := "|/-\\"
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nanos := now.UnixNano()
|
nanos := now.UnixNano()
|
||||||
@ -219,17 +220,26 @@ func loader() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: refactor properly
|
// TODO: refactor properly
|
||||||
func getFilesView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("files")
|
v, _ := g.View("files")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommitsView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("commits")
|
v, _ := g.View("commits")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommitMessageView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("commitMessage")
|
v, _ := g.View("commitMessage")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
||||||
|
return strings.TrimSpace(v.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) currentViewName(g *gocui.Gui) string {
|
||||||
|
currentView := g.CurrentView()
|
||||||
|
return currentView.Name()
|
||||||
|
}
|
65
pkg/utils/utils.go
Normal file
65
pkg/utils/utils.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SplitLines takes a multiline string and splits it on newlines
|
||||||
|
// currently we are also stripping \r's which may have adverse effects for
|
||||||
|
// windows users (but no issues have been raised yet)
|
||||||
|
func SplitLines(multilineString string) []string {
|
||||||
|
multilineString = strings.Replace(multilineString, "\r", "", -1)
|
||||||
|
if multilineString == "" || multilineString == "\n" {
|
||||||
|
return make([]string, 0)
|
||||||
|
}
|
||||||
|
lines := strings.Split(multilineString, "\n")
|
||||||
|
if lines[len(lines)-1] == "" {
|
||||||
|
return lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPadding pads a string as much as you want
|
||||||
|
func WithPadding(str string, padding int) string {
|
||||||
|
if padding-len(str) < 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return str + strings.Repeat(" ", padding-len(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColoredString takes a string and a colour attribute and returns a colored
|
||||||
|
// string with that attribute
|
||||||
|
func ColoredString(str string, colorAttribute color.Attribute) string {
|
||||||
|
colour := color.New(colorAttribute)
|
||||||
|
return ColoredStringDirect(str, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColoredStringDirect used for aggregating a few color attributes rather than
|
||||||
|
// just sending a single one
|
||||||
|
func ColoredStringDirect(str string, colour *color.Color) string {
|
||||||
|
return colour.SprintFunc()(fmt.Sprint(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentRepoName gets the repo's base name
|
||||||
|
func GetCurrentRepoName() string {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
return filepath.Base(pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimTrailingNewline - Trims the trailing newline
|
||||||
|
// TODO: replace with `chomp` after refactor
|
||||||
|
func TrimTrailingNewline(str string) string {
|
||||||
|
if strings.HasSuffix(str, "\n") {
|
||||||
|
return str[:len(str)-1]
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
@ -1,93 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func refreshStashEntries(g *gocui.Gui) error {
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
v, err := g.View("stash")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
state.StashEntries = getGitStashEntries()
|
|
||||||
v.Clear()
|
|
||||||
for _, stashEntry := range state.StashEntries {
|
|
||||||
fmt.Fprintln(v, stashEntry.DisplayString)
|
|
||||||
}
|
|
||||||
return resetOrigin(v)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedStashEntry(v *gocui.View) *StashEntry {
|
|
||||||
if len(state.StashEntries) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lineNumber := getItemPosition(v)
|
|
||||||
return &state.StashEntries[lineNumber]
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderStashOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"space": "apply",
|
|
||||||
"g": "pop",
|
|
||||||
"d": "drop",
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := renderStashOptions(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
stashEntry := getSelectedStashEntry(v)
|
|
||||||
if stashEntry == nil {
|
|
||||||
renderString(g, "main", "No stash entries")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diff, _ := getStashEntryDiff(stashEntry.Index)
|
|
||||||
renderString(g, "main", diff)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return stashDo(g, v, "apply")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return stashDo(g, v, "pop")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return stashDo(g, v, "drop")
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
|
||||||
stashEntry := getSelectedStashEntry(v)
|
|
||||||
if stashEntry == nil {
|
|
||||||
return createErrorPanel(g, "No stash to "+method)
|
|
||||||
}
|
|
||||||
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshStashEntries(g)
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
|
||||||
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitStashSave(trimmedContent(v)); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshStashEntries(g)
|
|
||||||
return refreshFiles(g)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
14
test/repos/gpg.sh
Executable file
14
test/repos/gpg.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||||
|
|
||||||
|
git init
|
||||||
|
|
||||||
|
git config gpg.program $(which gpg)
|
||||||
|
git config user.signingkey E304229F # test key
|
||||||
|
git config commit.gpgsign true
|
||||||
|
|
||||||
|
touch foo
|
||||||
|
git add foo
|
||||||
|
|
||||||
|
touch bar
|
||||||
|
git add bar
|
@ -2,7 +2,7 @@
|
|||||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||||
|
|
||||||
git init
|
git init
|
||||||
cp ../pre-commit .git/hooks/pre-commit
|
cp ../extras/pre-commit .git/hooks/pre-commit
|
||||||
chmod +x .git/hooks/pre-commit
|
chmod +x .git/hooks/pre-commit
|
||||||
|
|
||||||
echo "file" > file
|
echo "file" > file
|
||||||
|
54
utils.go
54
utils.go
@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func splitLines(multilineString string) []string {
|
|
||||||
multilineString = strings.Replace(multilineString, "\r", "", -1)
|
|
||||||
if multilineString == "" || multilineString == "\n" {
|
|
||||||
return make([]string, 0)
|
|
||||||
}
|
|
||||||
lines := strings.Split(multilineString, "\n")
|
|
||||||
if lines[len(lines)-1] == "" {
|
|
||||||
return lines[:len(lines)-1]
|
|
||||||
}
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimmedContent(v *gocui.View) string {
|
|
||||||
return strings.TrimSpace(v.Buffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
func withPadding(str string, padding int) string {
|
|
||||||
if padding-len(str) < 0 {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(" ", padding-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
func coloredString(str string, colorAttribute color.Attribute) string {
|
|
||||||
colour := color.New(colorAttribute)
|
|
||||||
return coloredStringDirect(str, colour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for aggregating a few color attributes rather than just sending a single one
|
|
||||||
func coloredStringDirect(str string, colour *color.Color) string {
|
|
||||||
return colour.SprintFunc()(fmt.Sprint(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to get the project name
|
|
||||||
func getCurrentProject() string {
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err.Error())
|
|
||||||
}
|
|
||||||
return filepath.Base(pwd)
|
|
||||||
}
|
|
21
vendor/github.com/Sirupsen/logrus/LICENSE
generated
vendored
Normal file
21
vendor/github.com/Sirupsen/logrus/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Simon Eskildsen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
64
vendor/github.com/Sirupsen/logrus/alt_exit.go
generated
vendored
Normal file
64
vendor/github.com/Sirupsen/logrus/alt_exit.go
generated
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
// The following code was sourced and modified from the
|
||||||
|
// https://github.com/tebeka/atexit package governed by the following license:
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
// subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handlers = []func(){}
|
||||||
|
|
||||||
|
func runHandler(handler func()) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHandlers() {
|
||||||
|
for _, handler := range handlers {
|
||||||
|
runHandler(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
|
||||||
|
func Exit(code int) {
|
||||||
|
runHandlers()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke
|
||||||
|
// all handlers. The handlers will also be invoked when any Fatal log entry is
|
||||||
|
// made.
|
||||||
|
//
|
||||||
|
// This method is useful when a caller wishes to use logrus to log a fatal
|
||||||
|
// message but also needs to gracefully shutdown. An example usecase could be
|
||||||
|
// closing database connections, or sending a alert that the application is
|
||||||
|
// closing.
|
||||||
|
func RegisterExitHandler(handler func()) {
|
||||||
|
handlers = append(handlers, handler)
|
||||||
|
}
|
26
vendor/github.com/Sirupsen/logrus/doc.go
generated
vendored
Normal file
26
vendor/github.com/Sirupsen/logrus/doc.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
|
||||||
|
|
||||||
|
|
||||||
|
The simplest way to use Logrus is simply the package-level exported logger:
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"number": 1,
|
||||||
|
"size": 10,
|
||||||
|
}).Info("A walrus appears")
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
|
||||||
|
|
||||||
|
For a full guide visit https://github.com/sirupsen/logrus
|
||||||
|
*/
|
||||||
|
package logrus
|
300
vendor/github.com/Sirupsen/logrus/entry.go
generated
vendored
Normal file
300
vendor/github.com/Sirupsen/logrus/entry.go
generated
vendored
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bufferPool *sync.Pool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
bufferPool = &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the key when adding errors using WithError.
|
||||||
|
var ErrorKey = "error"
|
||||||
|
|
||||||
|
// An entry is the final or intermediate Logrus logging entry. It contains all
|
||||||
|
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
|
||||||
|
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
|
||||||
|
// passed around as much as you wish to avoid field duplication.
|
||||||
|
type Entry struct {
|
||||||
|
Logger *Logger
|
||||||
|
|
||||||
|
// Contains all the fields set by the user.
|
||||||
|
Data Fields
|
||||||
|
|
||||||
|
// Time at which the log entry was created
|
||||||
|
Time time.Time
|
||||||
|
|
||||||
|
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
|
||||||
|
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
|
||||||
|
Level Level
|
||||||
|
|
||||||
|
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
|
||||||
|
Message string
|
||||||
|
|
||||||
|
// When formatter is called in entry.log(), an Buffer may be set to entry
|
||||||
|
Buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntry(logger *Logger) *Entry {
|
||||||
|
return &Entry{
|
||||||
|
Logger: logger,
|
||||||
|
// Default is five fields, give a little extra room
|
||||||
|
Data: make(Fields, 5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the string representation from the reader and ultimately the
|
||||||
|
// formatter.
|
||||||
|
func (entry *Entry) String() (string, error) {
|
||||||
|
serialized, err := entry.Logger.Formatter.Format(entry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
str := string(serialized)
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
|
||||||
|
func (entry *Entry) WithError(err error) *Entry {
|
||||||
|
return entry.WithField(ErrorKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a single field to the Entry.
|
||||||
|
func (entry *Entry) WithField(key string, value interface{}) *Entry {
|
||||||
|
return entry.WithFields(Fields{key: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a map of fields to the Entry.
|
||||||
|
func (entry *Entry) WithFields(fields Fields) *Entry {
|
||||||
|
data := make(Fields, len(entry.Data)+len(fields))
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range fields {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the time of the Entry.
|
||||||
|
func (entry *Entry) WithTime(t time.Time) *Entry {
|
||||||
|
return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is not declared with a pointer value because otherwise
|
||||||
|
// race conditions will occur when using multiple goroutines
|
||||||
|
func (entry Entry) log(level Level, msg string) {
|
||||||
|
var buffer *bytes.Buffer
|
||||||
|
|
||||||
|
// Default to now, but allow users to override if they want.
|
||||||
|
//
|
||||||
|
// We don't have to worry about polluting future calls to Entry#log()
|
||||||
|
// with this assignment because this function is declared with a
|
||||||
|
// non-pointer receiver.
|
||||||
|
if entry.Time.IsZero() {
|
||||||
|
entry.Time = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Level = level
|
||||||
|
entry.Message = msg
|
||||||
|
|
||||||
|
entry.fireHooks()
|
||||||
|
|
||||||
|
buffer = bufferPool.Get().(*bytes.Buffer)
|
||||||
|
buffer.Reset()
|
||||||
|
defer bufferPool.Put(buffer)
|
||||||
|
entry.Buffer = buffer
|
||||||
|
|
||||||
|
entry.write()
|
||||||
|
|
||||||
|
entry.Buffer = nil
|
||||||
|
|
||||||
|
// To avoid Entry#log() returning a value that only would make sense for
|
||||||
|
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
||||||
|
// directly here.
|
||||||
|
if level <= PanicLevel {
|
||||||
|
panic(&entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) fireHooks() {
|
||||||
|
entry.Logger.mu.Lock()
|
||||||
|
defer entry.Logger.mu.Unlock()
|
||||||
|
err := entry.Logger.Hooks.Fire(entry.Level, entry)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) write() {
|
||||||
|
serialized, err := entry.Logger.Formatter.Format(entry)
|
||||||
|
entry.Logger.mu.Lock()
|
||||||
|
defer entry.Logger.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
|
||||||
|
} else {
|
||||||
|
_, err = entry.Logger.Out.Write(serialized)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Debug(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= DebugLevel {
|
||||||
|
entry.log(DebugLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Print(args ...interface{}) {
|
||||||
|
entry.Info(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Info(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= InfoLevel {
|
||||||
|
entry.log(InfoLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warn(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= WarnLevel {
|
||||||
|
entry.log(WarnLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warning(args ...interface{}) {
|
||||||
|
entry.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Error(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= ErrorLevel {
|
||||||
|
entry.log(ErrorLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Fatal(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= FatalLevel {
|
||||||
|
entry.log(FatalLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Panic(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= PanicLevel {
|
||||||
|
entry.log(PanicLevel, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
panic(fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry Printf family functions
|
||||||
|
|
||||||
|
func (entry *Entry) Debugf(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= DebugLevel {
|
||||||
|
entry.Debug(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Infof(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= InfoLevel {
|
||||||
|
entry.Info(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Printf(format string, args ...interface{}) {
|
||||||
|
entry.Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warnf(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= WarnLevel {
|
||||||
|
entry.Warn(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warningf(format string, args ...interface{}) {
|
||||||
|
entry.Warnf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Errorf(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= ErrorLevel {
|
||||||
|
entry.Error(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Fatalf(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= FatalLevel {
|
||||||
|
entry.Fatal(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Panicf(format string, args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= PanicLevel {
|
||||||
|
entry.Panic(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry Println family functions
|
||||||
|
|
||||||
|
func (entry *Entry) Debugln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= DebugLevel {
|
||||||
|
entry.Debug(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Infoln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= InfoLevel {
|
||||||
|
entry.Info(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Println(args ...interface{}) {
|
||||||
|
entry.Infoln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warnln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= WarnLevel {
|
||||||
|
entry.Warn(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Warningln(args ...interface{}) {
|
||||||
|
entry.Warnln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Errorln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= ErrorLevel {
|
||||||
|
entry.Error(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Fatalln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= FatalLevel {
|
||||||
|
entry.Fatal(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Panicln(args ...interface{}) {
|
||||||
|
if entry.Logger.level() >= PanicLevel {
|
||||||
|
entry.Panic(entry.sprintlnn(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintlnn => Sprint no newline. This is to get the behavior of how
|
||||||
|
// fmt.Sprintln where spaces are always added between operands, regardless of
|
||||||
|
// their type. Instead of vendoring the Sprintln implementation to spare a
|
||||||
|
// string allocation, we do the simplest thing.
|
||||||
|
func (entry *Entry) sprintlnn(args ...interface{}) string {
|
||||||
|
msg := fmt.Sprintln(args...)
|
||||||
|
return msg[:len(msg)-1]
|
||||||
|
}
|
201
vendor/github.com/Sirupsen/logrus/exported.go
generated
vendored
Normal file
201
vendor/github.com/Sirupsen/logrus/exported.go
generated
vendored
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// std is the name of the standard logger in stdlib `log`
|
||||||
|
std = New()
|
||||||
|
)
|
||||||
|
|
||||||
|
func StandardLogger() *Logger {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput sets the standard logger output.
|
||||||
|
func SetOutput(out io.Writer) {
|
||||||
|
std.SetOutput(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormatter sets the standard logger formatter.
|
||||||
|
func SetFormatter(formatter Formatter) {
|
||||||
|
std.mu.Lock()
|
||||||
|
defer std.mu.Unlock()
|
||||||
|
std.Formatter = formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel sets the standard logger level.
|
||||||
|
func SetLevel(level Level) {
|
||||||
|
std.mu.Lock()
|
||||||
|
defer std.mu.Unlock()
|
||||||
|
std.SetLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevel returns the standard logger level.
|
||||||
|
func GetLevel() Level {
|
||||||
|
std.mu.Lock()
|
||||||
|
defer std.mu.Unlock()
|
||||||
|
return std.level()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHook adds a hook to the standard logger hooks.
|
||||||
|
func AddHook(hook Hook) {
|
||||||
|
std.mu.Lock()
|
||||||
|
defer std.mu.Unlock()
|
||||||
|
std.Hooks.Add(hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
|
||||||
|
func WithError(err error) *Entry {
|
||||||
|
return std.WithField(ErrorKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithField creates an entry from the standard logger and adds a field to
|
||||||
|
// it. If you want multiple fields, use `WithFields`.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithField(key string, value interface{}) *Entry {
|
||||||
|
return std.WithField(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFields creates an entry from the standard logger and adds multiple
|
||||||
|
// fields to it. This is simply a helper for `WithField`, invoking it
|
||||||
|
// once for each field.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithFields(fields Fields) *Entry {
|
||||||
|
return std.WithFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTime creats an entry from the standard logger and overrides the time of
|
||||||
|
// logs generated with it.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithTime(t time.Time) *Entry {
|
||||||
|
return std.WithTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a message at level Debug on the standard logger.
|
||||||
|
func Debug(args ...interface{}) {
|
||||||
|
std.Debug(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print logs a message at level Info on the standard logger.
|
||||||
|
func Print(args ...interface{}) {
|
||||||
|
std.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs a message at level Info on the standard logger.
|
||||||
|
func Info(args ...interface{}) {
|
||||||
|
std.Info(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a message at level Warn on the standard logger.
|
||||||
|
func Warn(args ...interface{}) {
|
||||||
|
std.Warn(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning logs a message at level Warn on the standard logger.
|
||||||
|
func Warning(args ...interface{}) {
|
||||||
|
std.Warning(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs a message at level Error on the standard logger.
|
||||||
|
func Error(args ...interface{}) {
|
||||||
|
std.Error(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panic logs a message at level Panic on the standard logger.
|
||||||
|
func Panic(args ...interface{}) {
|
||||||
|
std.Panic(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
|
func Fatal(args ...interface{}) {
|
||||||
|
std.Fatal(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a message at level Debug on the standard logger.
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
std.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf logs a message at level Info on the standard logger.
|
||||||
|
func Printf(format string, args ...interface{}) {
|
||||||
|
std.Printf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs a message at level Info on the standard logger.
|
||||||
|
func Infof(format string, args ...interface{}) {
|
||||||
|
std.Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a message at level Warn on the standard logger.
|
||||||
|
func Warnf(format string, args ...interface{}) {
|
||||||
|
std.Warnf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningf logs a message at level Warn on the standard logger.
|
||||||
|
func Warningf(format string, args ...interface{}) {
|
||||||
|
std.Warningf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs a message at level Error on the standard logger.
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
std.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicf logs a message at level Panic on the standard logger.
|
||||||
|
func Panicf(format string, args ...interface{}) {
|
||||||
|
std.Panicf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
|
func Fatalf(format string, args ...interface{}) {
|
||||||
|
std.Fatalf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugln logs a message at level Debug on the standard logger.
|
||||||
|
func Debugln(args ...interface{}) {
|
||||||
|
std.Debugln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println logs a message at level Info on the standard logger.
|
||||||
|
func Println(args ...interface{}) {
|
||||||
|
std.Println(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infoln logs a message at level Info on the standard logger.
|
||||||
|
func Infoln(args ...interface{}) {
|
||||||
|
std.Infoln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnln logs a message at level Warn on the standard logger.
|
||||||
|
func Warnln(args ...interface{}) {
|
||||||
|
std.Warnln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warningln logs a message at level Warn on the standard logger.
|
||||||
|
func Warningln(args ...interface{}) {
|
||||||
|
std.Warningln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorln logs a message at level Error on the standard logger.
|
||||||
|
func Errorln(args ...interface{}) {
|
||||||
|
std.Errorln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicln logs a message at level Panic on the standard logger.
|
||||||
|
func Panicln(args ...interface{}) {
|
||||||
|
std.Panicln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
|
func Fatalln(args ...interface{}) {
|
||||||
|
std.Fatalln(args...)
|
||||||
|
}
|
51
vendor/github.com/Sirupsen/logrus/formatter.go
generated
vendored
Normal file
51
vendor/github.com/Sirupsen/logrus/formatter.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const defaultTimestampFormat = time.RFC3339
|
||||||
|
|
||||||
|
// The Formatter interface is used to implement a custom Formatter. It takes an
|
||||||
|
// `Entry`. It exposes all the fields, including the default ones:
|
||||||
|
//
|
||||||
|
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
|
||||||
|
// * `entry.Data["time"]`. The timestamp.
|
||||||
|
// * `entry.Data["level"]. The level the entry was logged at.
|
||||||
|
//
|
||||||
|
// Any additional fields added with `WithField` or `WithFields` are also in
|
||||||
|
// `entry.Data`. Format is expected to return an array of bytes which are then
|
||||||
|
// logged to `logger.Out`.
|
||||||
|
type Formatter interface {
|
||||||
|
Format(*Entry) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is to not silently overwrite `time`, `msg` and `level` fields when
|
||||||
|
// dumping it. If this code wasn't there doing:
|
||||||
|
//
|
||||||
|
// logrus.WithField("level", 1).Info("hello")
|
||||||
|
//
|
||||||
|
// Would just silently drop the user provided level. Instead with this code
|
||||||
|
// it'll logged as:
|
||||||
|
//
|
||||||
|
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
|
||||||
|
//
|
||||||
|
// It's not exported because it's still using Data in an opinionated way. It's to
|
||||||
|
// avoid code duplication between the two default formatters.
|
||||||
|
func prefixFieldClashes(data Fields, fieldMap FieldMap) {
|
||||||
|
timeKey := fieldMap.resolve(FieldKeyTime)
|
||||||
|
if t, ok := data[timeKey]; ok {
|
||||||
|
data["fields."+timeKey] = t
|
||||||
|
delete(data, timeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgKey := fieldMap.resolve(FieldKeyMsg)
|
||||||
|
if m, ok := data[msgKey]; ok {
|
||||||
|
data["fields."+msgKey] = m
|
||||||
|
delete(data, msgKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
levelKey := fieldMap.resolve(FieldKeyLevel)
|
||||||
|
if l, ok := data[levelKey]; ok {
|
||||||
|
data["fields."+levelKey] = l
|
||||||
|
delete(data, levelKey)
|
||||||
|
}
|
||||||
|
}
|
34
vendor/github.com/Sirupsen/logrus/hooks.go
generated
vendored
Normal file
34
vendor/github.com/Sirupsen/logrus/hooks.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
// A hook to be fired when logging on the logging levels returned from
|
||||||
|
// `Levels()` on your implementation of the interface. Note that this is not
|
||||||
|
// fired in a goroutine or a channel with workers, you should handle such
|
||||||
|
// functionality yourself if your call is non-blocking and you don't wish for
|
||||||
|
// the logging calls for levels returned from `Levels()` to block.
|
||||||
|
type Hook interface {
|
||||||
|
Levels() []Level
|
||||||
|
Fire(*Entry) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal type for storing the hooks on a logger instance.
|
||||||
|
type LevelHooks map[Level][]Hook
|
||||||
|
|
||||||
|
// Add a hook to an instance of logger. This is called with
|
||||||
|
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
|
||||||
|
func (hooks LevelHooks) Add(hook Hook) {
|
||||||
|
for _, level := range hook.Levels() {
|
||||||
|
hooks[level] = append(hooks[level], hook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire all the hooks for the passed level. Used by `entry.log` to fire
|
||||||
|
// appropriate hooks for a log entry.
|
||||||
|
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
|
||||||
|
for _, hook := range hooks[level] {
|
||||||
|
if err := hook.Fire(entry); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
89
vendor/github.com/Sirupsen/logrus/json_formatter.go
generated
vendored
Normal file
89
vendor/github.com/Sirupsen/logrus/json_formatter.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fieldKey string
|
||||||
|
|
||||||
|
// FieldMap allows customization of the key names for default fields.
|
||||||
|
type FieldMap map[fieldKey]string
|
||||||
|
|
||||||
|
// Default key names for the default fields
|
||||||
|
const (
|
||||||
|
FieldKeyMsg = "msg"
|
||||||
|
FieldKeyLevel = "level"
|
||||||
|
FieldKeyTime = "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f FieldMap) resolve(key fieldKey) string {
|
||||||
|
if k, ok := f[key]; ok {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONFormatter formats logs into parsable json
|
||||||
|
type JSONFormatter struct {
|
||||||
|
// TimestampFormat sets the format used for marshaling timestamps.
|
||||||
|
TimestampFormat string
|
||||||
|
|
||||||
|
// DisableTimestamp allows disabling automatic timestamps in output
|
||||||
|
DisableTimestamp bool
|
||||||
|
|
||||||
|
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
|
||||||
|
DataKey string
|
||||||
|
|
||||||
|
// FieldMap allows users to customize the names of keys for default fields.
|
||||||
|
// As an example:
|
||||||
|
// formatter := &JSONFormatter{
|
||||||
|
// FieldMap: FieldMap{
|
||||||
|
// FieldKeyTime: "@timestamp",
|
||||||
|
// FieldKeyLevel: "@level",
|
||||||
|
// FieldKeyMsg: "@message",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
FieldMap FieldMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format renders a single log entry
|
||||||
|
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
|
data := make(Fields, len(entry.Data)+3)
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case error:
|
||||||
|
// Otherwise errors are ignored by `encoding/json`
|
||||||
|
// https://github.com/sirupsen/logrus/issues/137
|
||||||
|
data[k] = v.Error()
|
||||||
|
default:
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.DataKey != "" {
|
||||||
|
newData := make(Fields, 4)
|
||||||
|
newData[f.DataKey] = data
|
||||||
|
data = newData
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixFieldClashes(data, f.FieldMap)
|
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat
|
||||||
|
if timestampFormat == "" {
|
||||||
|
timestampFormat = defaultTimestampFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.DisableTimestamp {
|
||||||
|
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
|
||||||
|
}
|
||||||
|
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
|
||||||
|
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
|
||||||
|
|
||||||
|
serialized, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
||||||
|
}
|
||||||
|
return append(serialized, '\n'), nil
|
||||||
|
}
|
337
vendor/github.com/Sirupsen/logrus/logger.go
generated
vendored
Normal file
337
vendor/github.com/Sirupsen/logrus/logger.go
generated
vendored
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
|
||||||
|
// file, or leave it default which is `os.Stderr`. You can also set this to
|
||||||
|
// something more adventorous, such as logging to Kafka.
|
||||||
|
Out io.Writer
|
||||||
|
// Hooks for the logger instance. These allow firing events based on logging
|
||||||
|
// levels and log entries. For example, to send errors to an error tracking
|
||||||
|
// service, log to StatsD or dump the core on fatal errors.
|
||||||
|
Hooks LevelHooks
|
||||||
|
// All log entries pass through the formatter before logged to Out. The
|
||||||
|
// included formatters are `TextFormatter` and `JSONFormatter` for which
|
||||||
|
// TextFormatter is the default. In development (when a TTY is attached) it
|
||||||
|
// logs with colors, but to a file it wouldn't. You can easily implement your
|
||||||
|
// own that implements the `Formatter` interface, see the `README` or included
|
||||||
|
// formatters for examples.
|
||||||
|
Formatter Formatter
|
||||||
|
// The logging level the logger should log at. This is typically (and defaults
|
||||||
|
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
|
||||||
|
// logged.
|
||||||
|
Level Level
|
||||||
|
// Used to sync writing to the log. Locking is enabled by Default
|
||||||
|
mu MutexWrap
|
||||||
|
// Reusable empty entry
|
||||||
|
entryPool sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
type MutexWrap struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Lock() {
|
||||||
|
if !mw.disabled {
|
||||||
|
mw.lock.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Unlock() {
|
||||||
|
if !mw.disabled {
|
||||||
|
mw.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Disable() {
|
||||||
|
mw.disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new logger. Configuration should be set by changing `Formatter`,
|
||||||
|
// `Out` and `Hooks` directly on the default logger instance. You can also just
|
||||||
|
// instantiate your own:
|
||||||
|
//
|
||||||
|
// var log = &Logger{
|
||||||
|
// Out: os.Stderr,
|
||||||
|
// Formatter: new(JSONFormatter),
|
||||||
|
// Hooks: make(LevelHooks),
|
||||||
|
// Level: logrus.DebugLevel,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// It's recommended to make this a global instance called `log`.
|
||||||
|
func New() *Logger {
|
||||||
|
return &Logger{
|
||||||
|
Out: os.Stderr,
|
||||||
|
Formatter: new(TextFormatter),
|
||||||
|
Hooks: make(LevelHooks),
|
||||||
|
Level: InfoLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) newEntry() *Entry {
|
||||||
|
entry, ok := logger.entryPool.Get().(*Entry)
|
||||||
|
if ok {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
return NewEntry(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) releaseEntry(entry *Entry) {
|
||||||
|
logger.entryPool.Put(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a field to the log entry, note that it doesn't log until you call
|
||||||
|
// Debug, Print, Info, Warn, Error, Fatal or Panic. It only creates a log entry.
|
||||||
|
// If you want multiple fields, use `WithFields`.
|
||||||
|
func (logger *Logger) WithField(key string, value interface{}) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithField(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a struct of fields to the log entry. All it does is call `WithField` for
|
||||||
|
// each `Field`.
|
||||||
|
func (logger *Logger) WithFields(fields Fields) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an error as single field to the log entry. All it does is call
|
||||||
|
// `WithError` for the given `error`.
|
||||||
|
func (logger *Logger) WithError(err error) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the time of the log entry.
|
||||||
|
func (logger *Logger) WithTime(t time.Time) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Debugf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= DebugLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Debugf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Infof(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= InfoLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Infof(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Printf(format string, args ...interface{}) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Printf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warnf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warnf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warningf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warnf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Errorf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= ErrorLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Errorf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Fatalf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= FatalLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Fatalf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Panicf(format string, args ...interface{}) {
|
||||||
|
if logger.level() >= PanicLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Panicf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Debug(args ...interface{}) {
|
||||||
|
if logger.level() >= DebugLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Debug(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Info(args ...interface{}) {
|
||||||
|
if logger.level() >= InfoLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Info(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Print(args ...interface{}) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Info(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warn(args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warn(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warning(args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warn(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Error(args ...interface{}) {
|
||||||
|
if logger.level() >= ErrorLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Error(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Fatal(args ...interface{}) {
|
||||||
|
if logger.level() >= FatalLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Fatal(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Panic(args ...interface{}) {
|
||||||
|
if logger.level() >= PanicLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Panic(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Debugln(args ...interface{}) {
|
||||||
|
if logger.level() >= DebugLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Debugln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Infoln(args ...interface{}) {
|
||||||
|
if logger.level() >= InfoLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Infoln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Println(args ...interface{}) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Println(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warnln(args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warnln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Warningln(args ...interface{}) {
|
||||||
|
if logger.level() >= WarnLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Warnln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Errorln(args ...interface{}) {
|
||||||
|
if logger.level() >= ErrorLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Errorln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Fatalln(args ...interface{}) {
|
||||||
|
if logger.level() >= FatalLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Fatalln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Panicln(args ...interface{}) {
|
||||||
|
if logger.level() >= PanicLevel {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Panicln(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//When file is opened with appending mode, it's safe to
|
||||||
|
//write concurrently to a file (within 4k message on Linux).
|
||||||
|
//In these cases user can choose to disable the lock.
|
||||||
|
func (logger *Logger) SetNoLock() {
|
||||||
|
logger.mu.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) level() Level {
|
||||||
|
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) SetLevel(level Level) {
|
||||||
|
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) SetOutput(out io.Writer) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.Out = out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) AddHook(hook Hook) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.Hooks.Add(hook)
|
||||||
|
}
|
143
vendor/github.com/Sirupsen/logrus/logrus.go
generated
vendored
Normal file
143
vendor/github.com/Sirupsen/logrus/logrus.go
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fields type, used to pass to `WithFields`.
|
||||||
|
type Fields map[string]interface{}
|
||||||
|
|
||||||
|
// Level type
|
||||||
|
type Level uint32
|
||||||
|
|
||||||
|
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
|
||||||
|
func (level Level) String() string {
|
||||||
|
switch level {
|
||||||
|
case DebugLevel:
|
||||||
|
return "debug"
|
||||||
|
case InfoLevel:
|
||||||
|
return "info"
|
||||||
|
case WarnLevel:
|
||||||
|
return "warning"
|
||||||
|
case ErrorLevel:
|
||||||
|
return "error"
|
||||||
|
case FatalLevel:
|
||||||
|
return "fatal"
|
||||||
|
case PanicLevel:
|
||||||
|
return "panic"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLevel takes a string level and returns the Logrus log level constant.
|
||||||
|
func ParseLevel(lvl string) (Level, error) {
|
||||||
|
switch strings.ToLower(lvl) {
|
||||||
|
case "panic":
|
||||||
|
return PanicLevel, nil
|
||||||
|
case "fatal":
|
||||||
|
return FatalLevel, nil
|
||||||
|
case "error":
|
||||||
|
return ErrorLevel, nil
|
||||||
|
case "warn", "warning":
|
||||||
|
return WarnLevel, nil
|
||||||
|
case "info":
|
||||||
|
return InfoLevel, nil
|
||||||
|
case "debug":
|
||||||
|
return DebugLevel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var l Level
|
||||||
|
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A constant exposing all logging levels
|
||||||
|
var AllLevels = []Level{
|
||||||
|
PanicLevel,
|
||||||
|
FatalLevel,
|
||||||
|
ErrorLevel,
|
||||||
|
WarnLevel,
|
||||||
|
InfoLevel,
|
||||||
|
DebugLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are the different logging levels. You can set the logging level to log
|
||||||
|
// on your instance of logger, obtained with `logrus.New()`.
|
||||||
|
const (
|
||||||
|
// PanicLevel level, highest level of severity. Logs and then calls panic with the
|
||||||
|
// message passed to Debug, Info, ...
|
||||||
|
PanicLevel Level = iota
|
||||||
|
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
|
||||||
|
// logging level is set to Panic.
|
||||||
|
FatalLevel
|
||||||
|
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
|
||||||
|
// Commonly used for hooks to send errors to an error tracking service.
|
||||||
|
ErrorLevel
|
||||||
|
// WarnLevel level. Non-critical entries that deserve eyes.
|
||||||
|
WarnLevel
|
||||||
|
// InfoLevel level. General operational entries about what's going on inside the
|
||||||
|
// application.
|
||||||
|
InfoLevel
|
||||||
|
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
||||||
|
DebugLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
// Won't compile if StdLogger can't be realized by a log.Logger
|
||||||
|
var (
|
||||||
|
_ StdLogger = &log.Logger{}
|
||||||
|
_ StdLogger = &Entry{}
|
||||||
|
_ StdLogger = &Logger{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// StdLogger is what your logrus-enabled library should take, that way
|
||||||
|
// it'll accept a stdlib logger and a logrus logger. There's no standard
|
||||||
|
// interface, this is the closest we get, unfortunately.
|
||||||
|
type StdLogger interface {
|
||||||
|
Print(...interface{})
|
||||||
|
Printf(string, ...interface{})
|
||||||
|
Println(...interface{})
|
||||||
|
|
||||||
|
Fatal(...interface{})
|
||||||
|
Fatalf(string, ...interface{})
|
||||||
|
Fatalln(...interface{})
|
||||||
|
|
||||||
|
Panic(...interface{})
|
||||||
|
Panicf(string, ...interface{})
|
||||||
|
Panicln(...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FieldLogger interface generalizes the Entry and Logger types
|
||||||
|
type FieldLogger interface {
|
||||||
|
WithField(key string, value interface{}) *Entry
|
||||||
|
WithFields(fields Fields) *Entry
|
||||||
|
WithError(err error) *Entry
|
||||||
|
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Printf(format string, args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Warningf(format string, args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
Panicf(format string, args ...interface{})
|
||||||
|
|
||||||
|
Debug(args ...interface{})
|
||||||
|
Info(args ...interface{})
|
||||||
|
Print(args ...interface{})
|
||||||
|
Warn(args ...interface{})
|
||||||
|
Warning(args ...interface{})
|
||||||
|
Error(args ...interface{})
|
||||||
|
Fatal(args ...interface{})
|
||||||
|
Panic(args ...interface{})
|
||||||
|
|
||||||
|
Debugln(args ...interface{})
|
||||||
|
Infoln(args ...interface{})
|
||||||
|
Println(args ...interface{})
|
||||||
|
Warnln(args ...interface{})
|
||||||
|
Warningln(args ...interface{})
|
||||||
|
Errorln(args ...interface{})
|
||||||
|
Fatalln(args ...interface{})
|
||||||
|
Panicln(args ...interface{})
|
||||||
|
}
|
10
vendor/github.com/Sirupsen/logrus/terminal_bsd.go
generated
vendored
Normal file
10
vendor/github.com/Sirupsen/logrus/terminal_bsd.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// +build darwin freebsd openbsd netbsd dragonfly
|
||||||
|
// +build !appengine,!gopherjs
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TIOCGETA
|
||||||
|
|
||||||
|
type Termios unix.Termios
|
11
vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go
generated
vendored
Normal file
11
vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// +build appengine gopherjs
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
return true
|
||||||
|
}
|
19
vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go
generated
vendored
Normal file
19
vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// +build !appengine,!gopherjs
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
switch v := w.(type) {
|
||||||
|
case *os.File:
|
||||||
|
return terminal.IsTerminal(int(v.Fd()))
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
14
vendor/github.com/Sirupsen/logrus/terminal_linux.go
generated
vendored
Normal file
14
vendor/github.com/Sirupsen/logrus/terminal_linux.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Based on ssh/terminal:
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !appengine,!gopherjs
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TCGETS
|
||||||
|
|
||||||
|
type Termios unix.Termios
|
195
vendor/github.com/Sirupsen/logrus/text_formatter.go
generated
vendored
Normal file
195
vendor/github.com/Sirupsen/logrus/text_formatter.go
generated
vendored
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nocolor = 0
|
||||||
|
red = 31
|
||||||
|
green = 32
|
||||||
|
yellow = 33
|
||||||
|
blue = 36
|
||||||
|
gray = 37
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseTimestamp time.Time
|
||||||
|
emptyFieldMap FieldMap
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
baseTimestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextFormatter formats logs into text
|
||||||
|
type TextFormatter struct {
|
||||||
|
// Set to true to bypass checking for a TTY before outputting colors.
|
||||||
|
ForceColors bool
|
||||||
|
|
||||||
|
// Force disabling colors.
|
||||||
|
DisableColors bool
|
||||||
|
|
||||||
|
// Disable timestamp logging. useful when output is redirected to logging
|
||||||
|
// system that already adds timestamps.
|
||||||
|
DisableTimestamp bool
|
||||||
|
|
||||||
|
// Enable logging the full timestamp when a TTY is attached instead of just
|
||||||
|
// the time passed since beginning of execution.
|
||||||
|
FullTimestamp bool
|
||||||
|
|
||||||
|
// TimestampFormat to use for display when a full timestamp is printed
|
||||||
|
TimestampFormat string
|
||||||
|
|
||||||
|
// The fields are sorted by default for a consistent output. For applications
|
||||||
|
// that log extremely frequently and don't use the JSON formatter this may not
|
||||||
|
// be desired.
|
||||||
|
DisableSorting bool
|
||||||
|
|
||||||
|
// Disables the truncation of the level text to 4 characters.
|
||||||
|
DisableLevelTruncation bool
|
||||||
|
|
||||||
|
// QuoteEmptyFields will wrap empty fields in quotes if true
|
||||||
|
QuoteEmptyFields bool
|
||||||
|
|
||||||
|
// Whether the logger's out is to a terminal
|
||||||
|
isTerminal bool
|
||||||
|
|
||||||
|
// FieldMap allows users to customize the names of keys for default fields.
|
||||||
|
// As an example:
|
||||||
|
// formatter := &TextFormatter{
|
||||||
|
// FieldMap: FieldMap{
|
||||||
|
// FieldKeyTime: "@timestamp",
|
||||||
|
// FieldKeyLevel: "@level",
|
||||||
|
// FieldKeyMsg: "@message"}}
|
||||||
|
FieldMap FieldMap
|
||||||
|
|
||||||
|
sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TextFormatter) init(entry *Entry) {
|
||||||
|
if entry.Logger != nil {
|
||||||
|
f.isTerminal = checkIfTerminal(entry.Logger.Out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format renders a single log entry
|
||||||
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
|
prefixFieldClashes(entry.Data, f.FieldMap)
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(entry.Data))
|
||||||
|
for k := range entry.Data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.DisableSorting {
|
||||||
|
sort.Strings(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b *bytes.Buffer
|
||||||
|
if entry.Buffer != nil {
|
||||||
|
b = entry.Buffer
|
||||||
|
} else {
|
||||||
|
b = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Do(func() { f.init(entry) })
|
||||||
|
|
||||||
|
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
|
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat
|
||||||
|
if timestampFormat == "" {
|
||||||
|
timestampFormat = defaultTimestampFormat
|
||||||
|
}
|
||||||
|
if isColored {
|
||||||
|
f.printColored(b, entry, keys, timestampFormat)
|
||||||
|
} else {
|
||||||
|
if !f.DisableTimestamp {
|
||||||
|
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyTime), entry.Time.Format(timestampFormat))
|
||||||
|
}
|
||||||
|
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyLevel), entry.Level.String())
|
||||||
|
if entry.Message != "" {
|
||||||
|
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyMsg), entry.Message)
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
f.appendKeyValue(b, key, entry.Data[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteByte('\n')
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
|
||||||
|
var levelColor int
|
||||||
|
switch entry.Level {
|
||||||
|
case DebugLevel:
|
||||||
|
levelColor = gray
|
||||||
|
case WarnLevel:
|
||||||
|
levelColor = yellow
|
||||||
|
case ErrorLevel, FatalLevel, PanicLevel:
|
||||||
|
levelColor = red
|
||||||
|
default:
|
||||||
|
levelColor = blue
|
||||||
|
}
|
||||||
|
|
||||||
|
levelText := strings.ToUpper(entry.Level.String())
|
||||||
|
if !f.DisableLevelTruncation {
|
||||||
|
levelText = levelText[0:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.DisableTimestamp {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message)
|
||||||
|
} else if !f.FullTimestamp {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
v := entry.Data[k]
|
||||||
|
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
|
||||||
|
f.appendValue(b, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TextFormatter) needsQuoting(text string) bool {
|
||||||
|
if f.QuoteEmptyFields && len(text) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, ch := range text {
|
||||||
|
if !((ch >= 'a' && ch <= 'z') ||
|
||||||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
|
(ch >= '0' && ch <= '9') ||
|
||||||
|
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
||||||
|
if b.Len() > 0 {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(key)
|
||||||
|
b.WriteByte('=')
|
||||||
|
f.appendValue(b, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
||||||
|
stringVal, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
stringVal = fmt.Sprint(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.needsQuoting(stringVal) {
|
||||||
|
b.WriteString(stringVal)
|
||||||
|
} else {
|
||||||
|
b.WriteString(fmt.Sprintf("%q", stringVal))
|
||||||
|
}
|
||||||
|
}
|
62
vendor/github.com/Sirupsen/logrus/writer.go
generated
vendored
Normal file
62
vendor/github.com/Sirupsen/logrus/writer.go
generated
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (logger *Logger) Writer() *io.PipeWriter {
|
||||||
|
return logger.WriterLevel(InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
|
||||||
|
return NewEntry(logger).WriterLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Writer() *io.PipeWriter {
|
||||||
|
return entry.WriterLevel(InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
|
||||||
|
var printFunc func(args ...interface{})
|
||||||
|
|
||||||
|
switch level {
|
||||||
|
case DebugLevel:
|
||||||
|
printFunc = entry.Debug
|
||||||
|
case InfoLevel:
|
||||||
|
printFunc = entry.Info
|
||||||
|
case WarnLevel:
|
||||||
|
printFunc = entry.Warn
|
||||||
|
case ErrorLevel:
|
||||||
|
printFunc = entry.Error
|
||||||
|
case FatalLevel:
|
||||||
|
printFunc = entry.Fatal
|
||||||
|
case PanicLevel:
|
||||||
|
printFunc = entry.Panic
|
||||||
|
default:
|
||||||
|
printFunc = entry.Print
|
||||||
|
}
|
||||||
|
|
||||||
|
go entry.writerScanner(reader, printFunc)
|
||||||
|
runtime.SetFinalizer(writer, writerFinalizer)
|
||||||
|
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
printFunc(scanner.Text())
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
entry.Errorf("Error while reading from Writer: %s", err)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writerFinalizer(writer *io.PipeWriter) {
|
||||||
|
writer.Close()
|
||||||
|
}
|
3
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
3
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
@ -364,6 +364,9 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
|
|||||||
// MainLoop runs the main loop until an error is returned. A successful
|
// MainLoop runs the main loop until an error is returned. A successful
|
||||||
// finish should return ErrQuit.
|
// finish should return ErrQuit.
|
||||||
func (g *Gui) MainLoop() error {
|
func (g *Gui) MainLoop() error {
|
||||||
|
if err := g.flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
g.tbEvents <- termbox.PollEvent()
|
g.tbEvents <- termbox.PollEvent()
|
||||||
|
21
vendor/github.com/mgutz/str/LICENSE
generated
vendored
Normal file
21
vendor/github.com/mgutz/str/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013-2014 Mario L. Gutierrez <mario@mgutz.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
19
vendor/github.com/mgutz/str/doc.go
generated
vendored
Normal file
19
vendor/github.com/mgutz/str/doc.go
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Package str is a comprehensive set of string functions to build more
|
||||||
|
// Go awesomeness. Str complements Go's standard packages and does not duplicate
|
||||||
|
// functionality found in `strings` or `strconv`.
|
||||||
|
//
|
||||||
|
// Str is based on plain functions instead of object-based methods,
|
||||||
|
// consistent with Go standard string packages.
|
||||||
|
//
|
||||||
|
// str.Between("<a>foo</a>", "<a>", "</a>") == "foo"
|
||||||
|
//
|
||||||
|
// Str supports pipelining instead of chaining
|
||||||
|
//
|
||||||
|
// s := str.Pipe("\nabcdef\n", Clean, BetweenF("a", "f"), ChompLeftF("bc"))
|
||||||
|
//
|
||||||
|
// User-defined filters can be added to the pipeline by inserting a function
|
||||||
|
// or closure that returns a function with this signature
|
||||||
|
//
|
||||||
|
// func(string) string
|
||||||
|
//
|
||||||
|
package str
|
337
vendor/github.com/mgutz/str/funcsAO.go
generated
vendored
Normal file
337
vendor/github.com/mgutz/str/funcsAO.go
generated
vendored
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
package str
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
//"log"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verbose flag enables console output for those functions that have
|
||||||
|
// counterparts in Go's excellent stadard packages.
|
||||||
|
var Verbose = false
|
||||||
|
var templateOpen = "{{"
|
||||||
|
var templateClose = "}}"
|
||||||
|
|
||||||
|
var beginEndSpacesRe = regexp.MustCompile("^\\s+|\\s+$")
|
||||||
|
var camelizeRe = regexp.MustCompile(`(\-|_|\s)+(.)?`)
|
||||||
|
var camelizeRe2 = regexp.MustCompile(`(\-|_|\s)+`)
|
||||||
|
var capitalsRe = regexp.MustCompile("([A-Z])")
|
||||||
|
var dashSpaceRe = regexp.MustCompile(`[-\s]+`)
|
||||||
|
var dashesRe = regexp.MustCompile("-+")
|
||||||
|
var isAlphaNumericRe = regexp.MustCompile(`[^0-9a-z\xC0-\xFF]`)
|
||||||
|
var isAlphaRe = regexp.MustCompile(`[^a-z\xC0-\xFF]`)
|
||||||
|
var nWhitespaceRe = regexp.MustCompile(`\s+`)
|
||||||
|
var notDigitsRe = regexp.MustCompile(`[^0-9]`)
|
||||||
|
var slugifyRe = regexp.MustCompile(`[^\w\s\-]`)
|
||||||
|
var spaceUnderscoreRe = regexp.MustCompile("[_\\s]+")
|
||||||
|
var spacesRe = regexp.MustCompile("[\\s\\xA0]+")
|
||||||
|
var stripPuncRe = regexp.MustCompile(`[^\w\s]|_`)
|
||||||
|
var templateRe = regexp.MustCompile(`([\-\[\]()*\s])`)
|
||||||
|
var templateRe2 = regexp.MustCompile(`\$`)
|
||||||
|
var underscoreRe = regexp.MustCompile(`([a-z\d])([A-Z]+)`)
|
||||||
|
var whitespaceRe = regexp.MustCompile(`^[\s\xa0]*$`)
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Between extracts a string between left and right strings.
|
||||||
|
func Between(s, left, right string) string {
|
||||||
|
l := len(left)
|
||||||
|
startPos := strings.Index(s, left)
|
||||||
|
if startPos < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
endPos := IndexOf(s, right, startPos+l)
|
||||||
|
//log.Printf("%s: left %s right %s start %d end %d", s, left, right, startPos+l, endPos)
|
||||||
|
if endPos < 0 {
|
||||||
|
return ""
|
||||||
|
} else if right == "" {
|
||||||
|
return s[endPos:]
|
||||||
|
} else {
|
||||||
|
return s[startPos+l : endPos]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BetweenF is the filter form for Between.
|
||||||
|
func BetweenF(left, right string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Between(s, left, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camelize return new string which removes any underscores or dashes and convert a string into camel casing.
|
||||||
|
func Camelize(s string) string {
|
||||||
|
return camelizeRe.ReplaceAllStringFunc(s, func(val string) string {
|
||||||
|
val = strings.ToUpper(val)
|
||||||
|
val = camelizeRe2.ReplaceAllString(val, "")
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize uppercases the first char of s and lowercases the rest.
|
||||||
|
func Capitalize(s string) string {
|
||||||
|
return strings.ToUpper(s[0:1]) + strings.ToLower(s[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharAt returns a string from the character at the specified position.
|
||||||
|
func CharAt(s string, index int) string {
|
||||||
|
l := len(s)
|
||||||
|
shortcut := index < 0 || index > l-1 || l == 0
|
||||||
|
if shortcut {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s[index : index+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharAtF is the filter form of CharAt.
|
||||||
|
func CharAtF(index int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return CharAt(s, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChompLeft removes prefix at the start of a string.
|
||||||
|
func ChompLeft(s, prefix string) string {
|
||||||
|
if strings.HasPrefix(s, prefix) {
|
||||||
|
return s[len(prefix):]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChompLeftF is the filter form of ChompLeft.
|
||||||
|
func ChompLeftF(prefix string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return ChompLeft(s, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChompRight removes suffix from end of s.
|
||||||
|
func ChompRight(s, suffix string) string {
|
||||||
|
if strings.HasSuffix(s, suffix) {
|
||||||
|
return s[:len(s)-len(suffix)]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChompRightF is the filter form of ChompRight.
|
||||||
|
func ChompRightF(suffix string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return ChompRight(s, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify returns a camelized string with the first letter upper cased.
|
||||||
|
func Classify(s string) string {
|
||||||
|
return Camelize("-" + s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyF is the filter form of Classify.
|
||||||
|
func ClassifyF(s string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Classify(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean compresses all adjacent whitespace to a single space and trims s.
|
||||||
|
func Clean(s string) string {
|
||||||
|
s = spacesRe.ReplaceAllString(s, " ")
|
||||||
|
s = beginEndSpacesRe.ReplaceAllString(s, "")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dasherize converts a camel cased string into a string delimited by dashes.
|
||||||
|
func Dasherize(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
s = spaceUnderscoreRe.ReplaceAllString(s, "-")
|
||||||
|
s = capitalsRe.ReplaceAllString(s, "-$1")
|
||||||
|
s = dashesRe.ReplaceAllString(s, "-")
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeHTML is alias for html.EscapeString.
|
||||||
|
func EscapeHTML(s string) string {
|
||||||
|
if Verbose {
|
||||||
|
fmt.Println("Use html.EscapeString instead of EscapeHTML")
|
||||||
|
}
|
||||||
|
return html.EscapeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeHTMLEntities decodes HTML entities into their proper string representation.
|
||||||
|
// DecodeHTMLEntities is an alias for html.UnescapeString
|
||||||
|
func DecodeHTMLEntities(s string) string {
|
||||||
|
if Verbose {
|
||||||
|
fmt.Println("Use html.UnescapeString instead of DecodeHTMLEntities")
|
||||||
|
}
|
||||||
|
return html.UnescapeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsurePrefix ensures s starts with prefix.
|
||||||
|
func EnsurePrefix(s, prefix string) string {
|
||||||
|
if strings.HasPrefix(s, prefix) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return prefix + s
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsurePrefixF is the filter form of EnsurePrefix.
|
||||||
|
func EnsurePrefixF(prefix string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return EnsurePrefix(s, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureSuffix ensures s ends with suffix.
|
||||||
|
func EnsureSuffix(s, suffix string) string {
|
||||||
|
if strings.HasSuffix(s, suffix) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureSuffixF is the filter form of EnsureSuffix.
|
||||||
|
func EnsureSuffixF(suffix string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return EnsureSuffix(s, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Humanize transforms s into a human friendly form.
|
||||||
|
func Humanize(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s = Underscore(s)
|
||||||
|
var humanizeRe = regexp.MustCompile(`_id$`)
|
||||||
|
s = humanizeRe.ReplaceAllString(s, "")
|
||||||
|
s = strings.Replace(s, "_", " ", -1)
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
s = Capitalize(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iif is short for immediate if. If condition is true return truthy else falsey.
|
||||||
|
func Iif(condition bool, truthy string, falsey string) string {
|
||||||
|
if condition {
|
||||||
|
return truthy
|
||||||
|
}
|
||||||
|
return falsey
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOf finds the index of needle in s starting from start.
|
||||||
|
func IndexOf(s string, needle string, start int) int {
|
||||||
|
l := len(s)
|
||||||
|
if needle == "" {
|
||||||
|
if start < 0 {
|
||||||
|
return 0
|
||||||
|
} else if start < l {
|
||||||
|
return start
|
||||||
|
} else {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < 0 || start > l-1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
pos := strings.Index(s[start:], needle)
|
||||||
|
if pos == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return start + pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAlpha returns true if a string contains only letters from ASCII (a-z,A-Z). Other letters from other languages are not supported.
|
||||||
|
func IsAlpha(s string) bool {
|
||||||
|
return !isAlphaRe.MatchString(strings.ToLower(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAlphaNumeric returns true if a string contains letters and digits.
|
||||||
|
func IsAlphaNumeric(s string) bool {
|
||||||
|
return !isAlphaNumericRe.MatchString(strings.ToLower(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLower returns true if s comprised of all lower case characters.
|
||||||
|
func IsLower(s string) bool {
|
||||||
|
return IsAlpha(s) && s == strings.ToLower(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNumeric returns true if a string contains only digits from 0-9. Other digits not in Latin (such as Arabic) are not currently supported.
|
||||||
|
func IsNumeric(s string) bool {
|
||||||
|
return !notDigitsRe.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUpper returns true if s contains all upper case chracters.
|
||||||
|
func IsUpper(s string) bool {
|
||||||
|
return IsAlpha(s) && s == strings.ToUpper(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the string is solely composed of whitespace.
|
||||||
|
func IsEmpty(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return whitespaceRe.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left returns the left substring of length n.
|
||||||
|
func Left(s string, n int) string {
|
||||||
|
if n < 0 {
|
||||||
|
return Right(s, -n)
|
||||||
|
}
|
||||||
|
return Substr(s, 0, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeftF is the filter form of Left.
|
||||||
|
func LeftF(n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Left(s, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeftOf returns the substring left of needle.
|
||||||
|
func LeftOf(s string, needle string) string {
|
||||||
|
return Between(s, "", needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letters returns an array of runes as strings so it can be indexed into.
|
||||||
|
func Letters(s string) []string {
|
||||||
|
result := []string{}
|
||||||
|
for _, r := range s {
|
||||||
|
result = append(result, string(r))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines convert windows newlines to unix newlines then convert to an Array of lines.
|
||||||
|
func Lines(s string) []string {
|
||||||
|
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||||
|
return strings.Split(s, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map maps an array's iitem through an iterator.
|
||||||
|
func Map(arr []string, iterator func(string) string) []string {
|
||||||
|
r := []string{}
|
||||||
|
for _, item := range arr {
|
||||||
|
r = append(r, iterator(item))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns true if patterns matches the string
|
||||||
|
func Match(s, pattern string) bool {
|
||||||
|
r := regexp.MustCompile(pattern)
|
||||||
|
return r.MatchString(s)
|
||||||
|
}
|
534
vendor/github.com/mgutz/str/funcsPZ.go
generated
vendored
Normal file
534
vendor/github.com/mgutz/str/funcsPZ.go
generated
vendored
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
package str
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
//"log"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pad pads string s on both sides with c until it has length of n.
|
||||||
|
func Pad(s, c string, n int) string {
|
||||||
|
L := len(s)
|
||||||
|
if L >= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
n -= L
|
||||||
|
|
||||||
|
left := strings.Repeat(c, int(math.Ceil(float64(n)/2)))
|
||||||
|
right := strings.Repeat(c, int(math.Floor(float64(n)/2)))
|
||||||
|
return left + s + right
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadF is the filter form of Pad.
|
||||||
|
func PadF(c string, n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Pad(s, c, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadLeft pads s on left side with c until it has length of n.
|
||||||
|
func PadLeft(s, c string, n int) string {
|
||||||
|
L := len(s)
|
||||||
|
if L > n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strings.Repeat(c, (n-L)) + s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadLeftF is the filter form of PadLeft.
|
||||||
|
func PadLeftF(c string, n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return PadLeft(s, c, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadRight pads s on right side with c until it has length of n.
|
||||||
|
func PadRight(s, c string, n int) string {
|
||||||
|
L := len(s)
|
||||||
|
if L > n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s + strings.Repeat(c, n-L)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadRightF is the filter form of Padright
|
||||||
|
func PadRightF(c string, n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return PadRight(s, c, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipe pipes s through one or more string filters.
|
||||||
|
func Pipe(s string, funcs ...func(string) string) string {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
s = fn(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuoteItems quotes all items in array, mostly for debugging.
|
||||||
|
func QuoteItems(arr []string) []string {
|
||||||
|
return Map(arr, func(s string) string {
|
||||||
|
return strconv.Quote(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceF is the filter form of strings.Replace.
|
||||||
|
func ReplaceF(old, new string, n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return strings.Replace(s, old, new, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplacePattern replaces string with regexp string.
|
||||||
|
// ReplacePattern returns a copy of src, replacing matches of the Regexp with the replacement string repl. Inside repl, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch.
|
||||||
|
func ReplacePattern(s, pattern, repl string) string {
|
||||||
|
r := regexp.MustCompile(pattern)
|
||||||
|
return r.ReplaceAllString(s, repl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplacePatternF is the filter form of ReplaceRegexp.
|
||||||
|
func ReplacePatternF(pattern, repl string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return ReplacePattern(s, pattern, repl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse a string
|
||||||
|
func Reverse(s string) string {
|
||||||
|
cs := make([]rune, utf8.RuneCountInString(s))
|
||||||
|
i := len(cs)
|
||||||
|
for _, c := range s {
|
||||||
|
i--
|
||||||
|
cs[i] = c
|
||||||
|
}
|
||||||
|
return string(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right returns the right substring of length n.
|
||||||
|
func Right(s string, n int) string {
|
||||||
|
if n < 0 {
|
||||||
|
return Left(s, -n)
|
||||||
|
}
|
||||||
|
return Substr(s, len(s)-n, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RightF is the Filter version of Right.
|
||||||
|
func RightF(n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Right(s, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RightOf returns the substring to the right of prefix.
|
||||||
|
func RightOf(s string, prefix string) string {
|
||||||
|
return Between(s, prefix, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTemplateDelimiters sets the delimiters for Template function. Defaults to "{{" and "}}"
|
||||||
|
func SetTemplateDelimiters(opening, closing string) {
|
||||||
|
templateOpen = opening
|
||||||
|
templateClose = closing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice slices a string. If end is negative then it is the from the end
|
||||||
|
// of the string.
|
||||||
|
func Slice(s string, start, end int) string {
|
||||||
|
if end > -1 {
|
||||||
|
return s[start:end]
|
||||||
|
}
|
||||||
|
L := len(s)
|
||||||
|
if L+end > 0 {
|
||||||
|
return s[start : L-end]
|
||||||
|
}
|
||||||
|
return s[start:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceF is the filter for Slice.
|
||||||
|
func SliceF(start, end int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Slice(s, start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceContains determines whether val is an element in slice.
|
||||||
|
func SliceContains(slice []string, val string) bool {
|
||||||
|
if slice == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range slice {
|
||||||
|
if it == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceIndexOf gets the indx of val in slice. Returns -1 if not found.
|
||||||
|
func SliceIndexOf(slice []string, val string) int {
|
||||||
|
if slice == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, it := range slice {
|
||||||
|
if it == val {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slugify converts s into a dasherized string suitable for URL segment.
|
||||||
|
func Slugify(s string) string {
|
||||||
|
sl := slugifyRe.ReplaceAllString(s, "")
|
||||||
|
sl = strings.ToLower(sl)
|
||||||
|
sl = Dasherize(sl)
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripPunctuation strips puncation from string.
|
||||||
|
func StripPunctuation(s string) string {
|
||||||
|
s = stripPuncRe.ReplaceAllString(s, "")
|
||||||
|
s = nWhitespaceRe.ReplaceAllString(s, " ")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripTags strips all of the html tags or tags specified by the parameters
|
||||||
|
func StripTags(s string, tags ...string) string {
|
||||||
|
if len(tags) == 0 {
|
||||||
|
tags = append(tags, "")
|
||||||
|
}
|
||||||
|
for _, tag := range tags {
|
||||||
|
stripTagsRe := regexp.MustCompile(`(?i)<\/?` + tag + `[^<>]*>`)
|
||||||
|
s = stripTagsRe.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substr returns a substring of s starting at index of length n.
|
||||||
|
func Substr(s string, index int, n int) string {
|
||||||
|
L := len(s)
|
||||||
|
if index < 0 || index >= L || s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
end := index + n
|
||||||
|
if end >= L {
|
||||||
|
end = L
|
||||||
|
}
|
||||||
|
if end <= index {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s[index:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubstrF is the filter form of Substr.
|
||||||
|
func SubstrF(index, n int) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return Substr(s, index, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template is a string template which replaces template placeholders delimited
|
||||||
|
// by "{{" and "}}" with values from map. The global delimiters may be set with
|
||||||
|
// SetTemplateDelimiters.
|
||||||
|
func Template(s string, values map[string]interface{}) string {
|
||||||
|
return TemplateWithDelimiters(s, values, templateOpen, templateClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateDelimiters is the getter for the opening and closing delimiters for Template.
|
||||||
|
func TemplateDelimiters() (opening string, closing string) {
|
||||||
|
return templateOpen, templateClose
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateWithDelimiters is string template with user-defineable opening and closing delimiters.
|
||||||
|
func TemplateWithDelimiters(s string, values map[string]interface{}, opening, closing string) string {
|
||||||
|
escapeDelimiter := func(delim string) string {
|
||||||
|
result := templateRe.ReplaceAllString(delim, "\\$1")
|
||||||
|
return templateRe2.ReplaceAllString(result, "\\$")
|
||||||
|
}
|
||||||
|
|
||||||
|
openingDelim := escapeDelimiter(opening)
|
||||||
|
closingDelim := escapeDelimiter(closing)
|
||||||
|
r := regexp.MustCompile(openingDelim + `(.+?)` + closingDelim)
|
||||||
|
matches := r.FindAllStringSubmatch(s, -1)
|
||||||
|
for _, submatches := range matches {
|
||||||
|
match := submatches[0]
|
||||||
|
key := submatches[1]
|
||||||
|
//log.Printf("match %s key %s\n", match, key)
|
||||||
|
if values[key] != nil {
|
||||||
|
v := fmt.Sprintf("%v", values[key])
|
||||||
|
s = strings.Replace(s, match, v, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToArgv converts string s into an argv for exec.
|
||||||
|
func ToArgv(s string) []string {
|
||||||
|
const (
|
||||||
|
InArg = iota
|
||||||
|
InArgQuote
|
||||||
|
OutOfArg
|
||||||
|
)
|
||||||
|
currentState := OutOfArg
|
||||||
|
currentQuoteChar := "\x00" // to distinguish between ' and " quotations
|
||||||
|
// this allows to use "foo'bar"
|
||||||
|
currentArg := ""
|
||||||
|
argv := []string{}
|
||||||
|
|
||||||
|
isQuote := func(c string) bool {
|
||||||
|
return c == `"` || c == `'`
|
||||||
|
}
|
||||||
|
|
||||||
|
isEscape := func(c string) bool {
|
||||||
|
return c == `\`
|
||||||
|
}
|
||||||
|
|
||||||
|
isWhitespace := func(c string) bool {
|
||||||
|
return c == " " || c == "\t"
|
||||||
|
}
|
||||||
|
|
||||||
|
L := len(s)
|
||||||
|
for i := 0; i < L; i++ {
|
||||||
|
c := s[i : i+1]
|
||||||
|
|
||||||
|
//fmt.Printf("c %s state %v arg %s argv %v i %d\n", c, currentState, currentArg, args, i)
|
||||||
|
if isQuote(c) {
|
||||||
|
switch currentState {
|
||||||
|
case OutOfArg:
|
||||||
|
currentArg = ""
|
||||||
|
fallthrough
|
||||||
|
case InArg:
|
||||||
|
currentState = InArgQuote
|
||||||
|
currentQuoteChar = c
|
||||||
|
|
||||||
|
case InArgQuote:
|
||||||
|
if c == currentQuoteChar {
|
||||||
|
currentState = InArg
|
||||||
|
} else {
|
||||||
|
currentArg += c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if isWhitespace(c) {
|
||||||
|
switch currentState {
|
||||||
|
case InArg:
|
||||||
|
argv = append(argv, currentArg)
|
||||||
|
currentState = OutOfArg
|
||||||
|
case InArgQuote:
|
||||||
|
currentArg += c
|
||||||
|
case OutOfArg:
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if isEscape(c) {
|
||||||
|
switch currentState {
|
||||||
|
case OutOfArg:
|
||||||
|
currentArg = ""
|
||||||
|
currentState = InArg
|
||||||
|
fallthrough
|
||||||
|
case InArg:
|
||||||
|
fallthrough
|
||||||
|
case InArgQuote:
|
||||||
|
if i == L-1 {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// just add \ to end for windows
|
||||||
|
currentArg += c
|
||||||
|
} else {
|
||||||
|
panic("Escape character at end string")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
peek := s[i+1 : i+2]
|
||||||
|
if peek != `"` {
|
||||||
|
currentArg += c
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
c = s[i : i+1]
|
||||||
|
currentArg += c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch currentState {
|
||||||
|
case InArg, InArgQuote:
|
||||||
|
currentArg += c
|
||||||
|
|
||||||
|
case OutOfArg:
|
||||||
|
currentArg = ""
|
||||||
|
currentArg += c
|
||||||
|
currentState = InArg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentState == InArg {
|
||||||
|
argv = append(argv, currentArg)
|
||||||
|
} else if currentState == InArgQuote {
|
||||||
|
panic("Starting quote has no ending quote.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return argv
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBool fuzzily converts truthy values.
|
||||||
|
func ToBool(s string) bool {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
return s == "true" || s == "yes" || s == "on" || s == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBoolOr parses s as a bool or returns defaultValue.
|
||||||
|
func ToBoolOr(s string, defaultValue bool) bool {
|
||||||
|
b, err := strconv.ParseBool(s)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToIntOr parses s as an int or returns defaultValue.
|
||||||
|
func ToIntOr(s string, defaultValue int) int {
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat32Or parses as a float32 or returns defaultValue on error.
|
||||||
|
func ToFloat32Or(s string, defaultValue float32) float32 {
|
||||||
|
f, err := strconv.ParseFloat(s, 32)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return float32(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat64Or parses s as a float64 or returns defaultValue.
|
||||||
|
func ToFloat64Or(s string, defaultValue float64) float64 {
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloatOr parses as a float64 or returns defaultValue.
|
||||||
|
var ToFloatOr = ToFloat64Or
|
||||||
|
|
||||||
|
// TODO This is not working yet. Go's regexp package does not have some
|
||||||
|
// of the niceities in JavaScript
|
||||||
|
//
|
||||||
|
// Truncate truncates the string, accounting for word placement and chars count
|
||||||
|
// adding a morestr (defaults to ellipsis)
|
||||||
|
// func Truncate(s, morestr string, n int) string {
|
||||||
|
// L := len(s)
|
||||||
|
// if L <= n {
|
||||||
|
// return s
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if morestr == "" {
|
||||||
|
// morestr = "..."
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// tmpl := func(c string) string {
|
||||||
|
// if strings.ToUpper(c) != strings.ToLower(c) {
|
||||||
|
// return "A"
|
||||||
|
// }
|
||||||
|
// return " "
|
||||||
|
// }
|
||||||
|
// template := s[0 : n+1]
|
||||||
|
// var truncateRe = regexp.MustCompile(`.(?=\W*\w*$)`)
|
||||||
|
// truncateRe.ReplaceAllStringFunc(template, tmpl) // 'Hello, world' -> 'HellAA AAAAA'
|
||||||
|
// var wwRe = regexp.MustCompile(`\w\w`)
|
||||||
|
// var whitespaceRe2 = regexp.MustCompile(`\s*\S+$`)
|
||||||
|
// if wwRe.MatchString(template[len(template)-2:]) {
|
||||||
|
// template = whitespaceRe2.ReplaceAllString(template, "")
|
||||||
|
// } else {
|
||||||
|
// template = strings.TrimRight(template, " \t\n")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if len(template+morestr) > L {
|
||||||
|
// return s
|
||||||
|
// }
|
||||||
|
// return s[0:len(template)] + morestr
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// truncate: function(length, pruneStr) { //from underscore.string, author: github.com/rwz
|
||||||
|
// var str = this.s;
|
||||||
|
//
|
||||||
|
// length = ~~length;
|
||||||
|
// pruneStr = pruneStr || '...';
|
||||||
|
//
|
||||||
|
// if (str.length <= length) return new this.constructor(str);
|
||||||
|
//
|
||||||
|
// var tmpl = function(c){ return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' '; },
|
||||||
|
// template = str.slice(0, length+1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA'
|
||||||
|
//
|
||||||
|
// if (template.slice(template.length-2).match(/\w\w/))
|
||||||
|
// template = template.replace(/\s*\S+$/, '');
|
||||||
|
// else
|
||||||
|
// template = new S(template.slice(0, template.length-1)).trimRight().s;
|
||||||
|
//
|
||||||
|
// return (template+pruneStr).length > str.length ? new S(str) : new S(str.slice(0, template.length)+pruneStr);
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Underscore returns converted camel cased string into a string delimited by underscores.
|
||||||
|
func Underscore(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
u := strings.TrimSpace(s)
|
||||||
|
|
||||||
|
u = underscoreRe.ReplaceAllString(u, "${1}_$2")
|
||||||
|
u = dashSpaceRe.ReplaceAllString(u, "_")
|
||||||
|
u = strings.ToLower(u)
|
||||||
|
if IsUpper(s[0:1]) {
|
||||||
|
return "_" + u
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnescapeHTML is an alias for html.UnescapeString.
|
||||||
|
func UnescapeHTML(s string) string {
|
||||||
|
if Verbose {
|
||||||
|
fmt.Println("Use html.UnescapeString instead of UnescapeHTML")
|
||||||
|
}
|
||||||
|
return html.UnescapeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapHTML wraps s within HTML tag having attributes attrs. Note,
|
||||||
|
// WrapHTML does not escape s value.
|
||||||
|
func WrapHTML(s string, tag string, attrs map[string]string) string {
|
||||||
|
escapeHTMLAttributeQuotes := func(v string) string {
|
||||||
|
v = strings.Replace(v, "<", "<", -1)
|
||||||
|
v = strings.Replace(v, "&", "&", -1)
|
||||||
|
v = strings.Replace(v, "\"", """, -1)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if tag == "" {
|
||||||
|
tag = "div"
|
||||||
|
}
|
||||||
|
el := "<" + tag
|
||||||
|
for name, val := range attrs {
|
||||||
|
el += " " + name + "=\"" + escapeHTMLAttributeQuotes(val) + "\""
|
||||||
|
}
|
||||||
|
el += ">" + s + "</" + tag + ">"
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapHTMLF is the filter form of WrapHTML.
|
||||||
|
func WrapHTMLF(tag string, attrs map[string]string) func(string) string {
|
||||||
|
return func(s string) string {
|
||||||
|
return WrapHTML(s, tag, attrs)
|
||||||
|
}
|
||||||
|
}
|
951
vendor/golang.org/x/crypto/ssh/terminal/terminal.go
generated
vendored
Normal file
951
vendor/golang.org/x/crypto/ssh/terminal/terminal.go
generated
vendored
Normal file
@ -0,0 +1,951 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeCodes contains escape sequences that can be written to the terminal in
|
||||||
|
// order to achieve different styles of text.
|
||||||
|
type EscapeCodes struct {
|
||||||
|
// Foreground colors
|
||||||
|
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
|
||||||
|
|
||||||
|
// Reset all attributes
|
||||||
|
Reset []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var vt100EscapeCodes = EscapeCodes{
|
||||||
|
Black: []byte{keyEscape, '[', '3', '0', 'm'},
|
||||||
|
Red: []byte{keyEscape, '[', '3', '1', 'm'},
|
||||||
|
Green: []byte{keyEscape, '[', '3', '2', 'm'},
|
||||||
|
Yellow: []byte{keyEscape, '[', '3', '3', 'm'},
|
||||||
|
Blue: []byte{keyEscape, '[', '3', '4', 'm'},
|
||||||
|
Magenta: []byte{keyEscape, '[', '3', '5', 'm'},
|
||||||
|
Cyan: []byte{keyEscape, '[', '3', '6', 'm'},
|
||||||
|
White: []byte{keyEscape, '[', '3', '7', 'm'},
|
||||||
|
|
||||||
|
Reset: []byte{keyEscape, '[', '0', 'm'},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal contains the state for running a VT100 terminal that is capable of
|
||||||
|
// reading lines of input.
|
||||||
|
type Terminal struct {
|
||||||
|
// AutoCompleteCallback, if non-null, is called for each keypress with
|
||||||
|
// the full input line and the current position of the cursor (in
|
||||||
|
// bytes, as an index into |line|). If it returns ok=false, the key
|
||||||
|
// press is processed normally. Otherwise it returns a replacement line
|
||||||
|
// and the new cursor position.
|
||||||
|
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
|
||||||
|
|
||||||
|
// Escape contains a pointer to the escape codes for this terminal.
|
||||||
|
// It's always a valid pointer, although the escape codes themselves
|
||||||
|
// may be empty if the terminal doesn't support them.
|
||||||
|
Escape *EscapeCodes
|
||||||
|
|
||||||
|
// lock protects the terminal and the state in this object from
|
||||||
|
// concurrent processing of a key press and a Write() call.
|
||||||
|
lock sync.Mutex
|
||||||
|
|
||||||
|
c io.ReadWriter
|
||||||
|
prompt []rune
|
||||||
|
|
||||||
|
// line is the current line being entered.
|
||||||
|
line []rune
|
||||||
|
// pos is the logical position of the cursor in line
|
||||||
|
pos int
|
||||||
|
// echo is true if local echo is enabled
|
||||||
|
echo bool
|
||||||
|
// pasteActive is true iff there is a bracketed paste operation in
|
||||||
|
// progress.
|
||||||
|
pasteActive bool
|
||||||
|
|
||||||
|
// cursorX contains the current X value of the cursor where the left
|
||||||
|
// edge is 0. cursorY contains the row number where the first row of
|
||||||
|
// the current line is 0.
|
||||||
|
cursorX, cursorY int
|
||||||
|
// maxLine is the greatest value of cursorY so far.
|
||||||
|
maxLine int
|
||||||
|
|
||||||
|
termWidth, termHeight int
|
||||||
|
|
||||||
|
// outBuf contains the terminal data to be sent.
|
||||||
|
outBuf []byte
|
||||||
|
// remainder contains the remainder of any partial key sequences after
|
||||||
|
// a read. It aliases into inBuf.
|
||||||
|
remainder []byte
|
||||||
|
inBuf [256]byte
|
||||||
|
|
||||||
|
// history contains previously entered commands so that they can be
|
||||||
|
// accessed with the up and down keys.
|
||||||
|
history stRingBuffer
|
||||||
|
// historyIndex stores the currently accessed history entry, where zero
|
||||||
|
// means the immediately previous entry.
|
||||||
|
historyIndex int
|
||||||
|
// When navigating up and down the history it's possible to return to
|
||||||
|
// the incomplete, initial line. That value is stored in
|
||||||
|
// historyPending.
|
||||||
|
historyPending string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
|
||||||
|
// a local terminal, that terminal must first have been put into raw mode.
|
||||||
|
// prompt is a string that is written at the start of each input line (i.e.
|
||||||
|
// "> ").
|
||||||
|
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
|
||||||
|
return &Terminal{
|
||||||
|
Escape: &vt100EscapeCodes,
|
||||||
|
c: c,
|
||||||
|
prompt: []rune(prompt),
|
||||||
|
termWidth: 80,
|
||||||
|
termHeight: 24,
|
||||||
|
echo: true,
|
||||||
|
historyIndex: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyCtrlD = 4
|
||||||
|
keyCtrlU = 21
|
||||||
|
keyEnter = '\r'
|
||||||
|
keyEscape = 27
|
||||||
|
keyBackspace = 127
|
||||||
|
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
|
||||||
|
keyUp
|
||||||
|
keyDown
|
||||||
|
keyLeft
|
||||||
|
keyRight
|
||||||
|
keyAltLeft
|
||||||
|
keyAltRight
|
||||||
|
keyHome
|
||||||
|
keyEnd
|
||||||
|
keyDeleteWord
|
||||||
|
keyDeleteLine
|
||||||
|
keyClearScreen
|
||||||
|
keyPasteStart
|
||||||
|
keyPasteEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
crlf = []byte{'\r', '\n'}
|
||||||
|
pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'}
|
||||||
|
pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'}
|
||||||
|
)
|
||||||
|
|
||||||
|
// bytesToKey tries to parse a key sequence from b. If successful, it returns
|
||||||
|
// the key and the remainder of the input. Otherwise it returns utf8.RuneError.
|
||||||
|
func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return utf8.RuneError, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pasteActive {
|
||||||
|
switch b[0] {
|
||||||
|
case 1: // ^A
|
||||||
|
return keyHome, b[1:]
|
||||||
|
case 5: // ^E
|
||||||
|
return keyEnd, b[1:]
|
||||||
|
case 8: // ^H
|
||||||
|
return keyBackspace, b[1:]
|
||||||
|
case 11: // ^K
|
||||||
|
return keyDeleteLine, b[1:]
|
||||||
|
case 12: // ^L
|
||||||
|
return keyClearScreen, b[1:]
|
||||||
|
case 23: // ^W
|
||||||
|
return keyDeleteWord, b[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[0] != keyEscape {
|
||||||
|
if !utf8.FullRune(b) {
|
||||||
|
return utf8.RuneError, b
|
||||||
|
}
|
||||||
|
r, l := utf8.DecodeRune(b)
|
||||||
|
return r, b[l:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
|
||||||
|
switch b[2] {
|
||||||
|
case 'A':
|
||||||
|
return keyUp, b[3:]
|
||||||
|
case 'B':
|
||||||
|
return keyDown, b[3:]
|
||||||
|
case 'C':
|
||||||
|
return keyRight, b[3:]
|
||||||
|
case 'D':
|
||||||
|
return keyLeft, b[3:]
|
||||||
|
case 'H':
|
||||||
|
return keyHome, b[3:]
|
||||||
|
case 'F':
|
||||||
|
return keyEnd, b[3:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
|
||||||
|
switch b[5] {
|
||||||
|
case 'C':
|
||||||
|
return keyAltRight, b[6:]
|
||||||
|
case 'D':
|
||||||
|
return keyAltLeft, b[6:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) {
|
||||||
|
return keyPasteStart, b[6:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) {
|
||||||
|
return keyPasteEnd, b[6:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here then we have a key that we don't recognise, or a
|
||||||
|
// partial sequence. It's not clear how one should find the end of a
|
||||||
|
// sequence without knowing them all, but it seems that [a-zA-Z~] only
|
||||||
|
// appears at the end of a sequence.
|
||||||
|
for i, c := range b[0:] {
|
||||||
|
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' {
|
||||||
|
return keyUnknown, b[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utf8.RuneError, b
|
||||||
|
}
|
||||||
|
|
||||||
|
// queue appends data to the end of t.outBuf
|
||||||
|
func (t *Terminal) queue(data []rune) {
|
||||||
|
t.outBuf = append(t.outBuf, []byte(string(data))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'}
|
||||||
|
var space = []rune{' '}
|
||||||
|
|
||||||
|
func isPrintable(key rune) bool {
|
||||||
|
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
|
||||||
|
return key >= 32 && !isInSurrogateArea
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveCursorToPos appends data to t.outBuf which will move the cursor to the
|
||||||
|
// given, logical position in the text.
|
||||||
|
func (t *Terminal) moveCursorToPos(pos int) {
|
||||||
|
if !t.echo {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x := visualLength(t.prompt) + pos
|
||||||
|
y := x / t.termWidth
|
||||||
|
x = x % t.termWidth
|
||||||
|
|
||||||
|
up := 0
|
||||||
|
if y < t.cursorY {
|
||||||
|
up = t.cursorY - y
|
||||||
|
}
|
||||||
|
|
||||||
|
down := 0
|
||||||
|
if y > t.cursorY {
|
||||||
|
down = y - t.cursorY
|
||||||
|
}
|
||||||
|
|
||||||
|
left := 0
|
||||||
|
if x < t.cursorX {
|
||||||
|
left = t.cursorX - x
|
||||||
|
}
|
||||||
|
|
||||||
|
right := 0
|
||||||
|
if x > t.cursorX {
|
||||||
|
right = x - t.cursorX
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cursorX = x
|
||||||
|
t.cursorY = y
|
||||||
|
t.move(up, down, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) move(up, down, left, right int) {
|
||||||
|
movement := make([]rune, 3*(up+down+left+right))
|
||||||
|
m := movement
|
||||||
|
for i := 0; i < up; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'A'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < down; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'B'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < left; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'D'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < right; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'C'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
|
||||||
|
t.queue(movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) clearLineToRight() {
|
||||||
|
op := []rune{keyEscape, '[', 'K'}
|
||||||
|
t.queue(op)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLineLength = 4096
|
||||||
|
|
||||||
|
func (t *Terminal) setLine(newLine []rune, newPos int) {
|
||||||
|
if t.echo {
|
||||||
|
t.moveCursorToPos(0)
|
||||||
|
t.writeLine(newLine)
|
||||||
|
for i := len(newLine); i < len(t.line); i++ {
|
||||||
|
t.writeLine(space)
|
||||||
|
}
|
||||||
|
t.moveCursorToPos(newPos)
|
||||||
|
}
|
||||||
|
t.line = newLine
|
||||||
|
t.pos = newPos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) advanceCursor(places int) {
|
||||||
|
t.cursorX += places
|
||||||
|
t.cursorY += t.cursorX / t.termWidth
|
||||||
|
if t.cursorY > t.maxLine {
|
||||||
|
t.maxLine = t.cursorY
|
||||||
|
}
|
||||||
|
t.cursorX = t.cursorX % t.termWidth
|
||||||
|
|
||||||
|
if places > 0 && t.cursorX == 0 {
|
||||||
|
// Normally terminals will advance the current position
|
||||||
|
// when writing a character. But that doesn't happen
|
||||||
|
// for the last character in a line. However, when
|
||||||
|
// writing a character (except a new line) that causes
|
||||||
|
// a line wrap, the position will be advanced two
|
||||||
|
// places.
|
||||||
|
//
|
||||||
|
// So, if we are stopping at the end of a line, we
|
||||||
|
// need to write a newline so that our cursor can be
|
||||||
|
// advanced to the next line.
|
||||||
|
t.outBuf = append(t.outBuf, '\r', '\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) eraseNPreviousChars(n int) {
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.pos < n {
|
||||||
|
n = t.pos
|
||||||
|
}
|
||||||
|
t.pos -= n
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
|
||||||
|
copy(t.line[t.pos:], t.line[n+t.pos:])
|
||||||
|
t.line = t.line[:len(t.line)-n]
|
||||||
|
if t.echo {
|
||||||
|
t.writeLine(t.line[t.pos:])
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
t.queue(space)
|
||||||
|
}
|
||||||
|
t.advanceCursor(n)
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// countToLeftWord returns then number of characters from the cursor to the
|
||||||
|
// start of the previous word.
|
||||||
|
func (t *Terminal) countToLeftWord() int {
|
||||||
|
if t.pos == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := t.pos - 1
|
||||||
|
for pos > 0 {
|
||||||
|
if t.line[pos] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
for pos > 0 {
|
||||||
|
if t.line[pos] == ' ' {
|
||||||
|
pos++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.pos - pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// countToRightWord returns then number of characters from the cursor to the
|
||||||
|
// start of the next word.
|
||||||
|
func (t *Terminal) countToRightWord() int {
|
||||||
|
pos := t.pos
|
||||||
|
for pos < len(t.line) {
|
||||||
|
if t.line[pos] == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
for pos < len(t.line) {
|
||||||
|
if t.line[pos] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
return pos - t.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// visualLength returns the number of visible glyphs in s.
|
||||||
|
func visualLength(runes []rune) int {
|
||||||
|
inEscapeSeq := false
|
||||||
|
length := 0
|
||||||
|
|
||||||
|
for _, r := range runes {
|
||||||
|
switch {
|
||||||
|
case inEscapeSeq:
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||||||
|
inEscapeSeq = false
|
||||||
|
}
|
||||||
|
case r == '\x1b':
|
||||||
|
inEscapeSeq = true
|
||||||
|
default:
|
||||||
|
length++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleKey processes the given key and, optionally, returns a line of text
|
||||||
|
// that the user has entered.
|
||||||
|
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
|
||||||
|
if t.pasteActive && key != keyEnter {
|
||||||
|
t.addKeyToLine(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case keyBackspace:
|
||||||
|
if t.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.eraseNPreviousChars(1)
|
||||||
|
case keyAltLeft:
|
||||||
|
// move left by a word.
|
||||||
|
t.pos -= t.countToLeftWord()
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyAltRight:
|
||||||
|
// move right by a word.
|
||||||
|
t.pos += t.countToRightWord()
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyLeft:
|
||||||
|
if t.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.pos--
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyRight:
|
||||||
|
if t.pos == len(t.line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.pos++
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyHome:
|
||||||
|
if t.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.pos = 0
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyEnd:
|
||||||
|
if t.pos == len(t.line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.pos = len(t.line)
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyUp:
|
||||||
|
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if t.historyIndex == -1 {
|
||||||
|
t.historyPending = string(t.line)
|
||||||
|
}
|
||||||
|
t.historyIndex++
|
||||||
|
runes := []rune(entry)
|
||||||
|
t.setLine(runes, len(runes))
|
||||||
|
case keyDown:
|
||||||
|
switch t.historyIndex {
|
||||||
|
case -1:
|
||||||
|
return
|
||||||
|
case 0:
|
||||||
|
runes := []rune(t.historyPending)
|
||||||
|
t.setLine(runes, len(runes))
|
||||||
|
t.historyIndex--
|
||||||
|
default:
|
||||||
|
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
|
||||||
|
if ok {
|
||||||
|
t.historyIndex--
|
||||||
|
runes := []rune(entry)
|
||||||
|
t.setLine(runes, len(runes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case keyEnter:
|
||||||
|
t.moveCursorToPos(len(t.line))
|
||||||
|
t.queue([]rune("\r\n"))
|
||||||
|
line = string(t.line)
|
||||||
|
ok = true
|
||||||
|
t.line = t.line[:0]
|
||||||
|
t.pos = 0
|
||||||
|
t.cursorX = 0
|
||||||
|
t.cursorY = 0
|
||||||
|
t.maxLine = 0
|
||||||
|
case keyDeleteWord:
|
||||||
|
// Delete zero or more spaces and then one or more characters.
|
||||||
|
t.eraseNPreviousChars(t.countToLeftWord())
|
||||||
|
case keyDeleteLine:
|
||||||
|
// Delete everything from the current cursor position to the
|
||||||
|
// end of line.
|
||||||
|
for i := t.pos; i < len(t.line); i++ {
|
||||||
|
t.queue(space)
|
||||||
|
t.advanceCursor(1)
|
||||||
|
}
|
||||||
|
t.line = t.line[:t.pos]
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
case keyCtrlD:
|
||||||
|
// Erase the character under the current position.
|
||||||
|
// The EOF case when the line is empty is handled in
|
||||||
|
// readLine().
|
||||||
|
if t.pos < len(t.line) {
|
||||||
|
t.pos++
|
||||||
|
t.eraseNPreviousChars(1)
|
||||||
|
}
|
||||||
|
case keyCtrlU:
|
||||||
|
t.eraseNPreviousChars(t.pos)
|
||||||
|
case keyClearScreen:
|
||||||
|
// Erases the screen and moves the cursor to the home position.
|
||||||
|
t.queue([]rune("\x1b[2J\x1b[H"))
|
||||||
|
t.queue(t.prompt)
|
||||||
|
t.cursorX, t.cursorY = 0, 0
|
||||||
|
t.advanceCursor(visualLength(t.prompt))
|
||||||
|
t.setLine(t.line, t.pos)
|
||||||
|
default:
|
||||||
|
if t.AutoCompleteCallback != nil {
|
||||||
|
prefix := string(t.line[:t.pos])
|
||||||
|
suffix := string(t.line[t.pos:])
|
||||||
|
|
||||||
|
t.lock.Unlock()
|
||||||
|
newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key)
|
||||||
|
t.lock.Lock()
|
||||||
|
|
||||||
|
if completeOk {
|
||||||
|
t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isPrintable(key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(t.line) == maxLineLength {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.addKeyToLine(key)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// addKeyToLine inserts the given key at the current position in the current
|
||||||
|
// line.
|
||||||
|
func (t *Terminal) addKeyToLine(key rune) {
|
||||||
|
if len(t.line) == cap(t.line) {
|
||||||
|
newLine := make([]rune, len(t.line), 2*(1+len(t.line)))
|
||||||
|
copy(newLine, t.line)
|
||||||
|
t.line = newLine
|
||||||
|
}
|
||||||
|
t.line = t.line[:len(t.line)+1]
|
||||||
|
copy(t.line[t.pos+1:], t.line[t.pos:])
|
||||||
|
t.line[t.pos] = key
|
||||||
|
if t.echo {
|
||||||
|
t.writeLine(t.line[t.pos:])
|
||||||
|
}
|
||||||
|
t.pos++
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) writeLine(line []rune) {
|
||||||
|
for len(line) != 0 {
|
||||||
|
remainingOnLine := t.termWidth - t.cursorX
|
||||||
|
todo := len(line)
|
||||||
|
if todo > remainingOnLine {
|
||||||
|
todo = remainingOnLine
|
||||||
|
}
|
||||||
|
t.queue(line[:todo])
|
||||||
|
t.advanceCursor(visualLength(line[:todo]))
|
||||||
|
line = line[todo:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n.
|
||||||
|
func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) {
|
||||||
|
for len(buf) > 0 {
|
||||||
|
i := bytes.IndexByte(buf, '\n')
|
||||||
|
todo := len(buf)
|
||||||
|
if i >= 0 {
|
||||||
|
todo = i
|
||||||
|
}
|
||||||
|
|
||||||
|
var nn int
|
||||||
|
nn, err = w.Write(buf[:todo])
|
||||||
|
n += nn
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
buf = buf[todo:]
|
||||||
|
|
||||||
|
if i >= 0 {
|
||||||
|
if _, err = w.Write(crlf); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
buf = buf[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Write(buf []byte) (n int, err error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
if t.cursorX == 0 && t.cursorY == 0 {
|
||||||
|
// This is the easy case: there's nothing on the screen that we
|
||||||
|
// have to move out of the way.
|
||||||
|
return writeWithCRLF(t.c, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a prompt and possibly user input on the screen. We
|
||||||
|
// have to clear it first.
|
||||||
|
t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */)
|
||||||
|
t.cursorX = 0
|
||||||
|
t.clearLineToRight()
|
||||||
|
|
||||||
|
for t.cursorY > 0 {
|
||||||
|
t.move(1 /* up */, 0, 0, 0)
|
||||||
|
t.cursorY--
|
||||||
|
t.clearLineToRight()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = t.c.Write(t.outBuf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
|
||||||
|
if n, err = writeWithCRLF(t.c, buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.writeLine(t.prompt)
|
||||||
|
if t.echo {
|
||||||
|
t.writeLine(t.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
|
||||||
|
if _, err = t.c.Write(t.outBuf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword temporarily changes the prompt and reads a password, without
|
||||||
|
// echo, from the terminal.
|
||||||
|
func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
oldPrompt := t.prompt
|
||||||
|
t.prompt = []rune(prompt)
|
||||||
|
t.echo = false
|
||||||
|
|
||||||
|
line, err = t.readLine()
|
||||||
|
|
||||||
|
t.prompt = oldPrompt
|
||||||
|
t.echo = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLine returns a line of input from the terminal.
|
||||||
|
func (t *Terminal) ReadLine() (line string, err error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
return t.readLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) readLine() (line string, err error) {
|
||||||
|
// t.lock must be held at this point
|
||||||
|
|
||||||
|
if t.cursorX == 0 && t.cursorY == 0 {
|
||||||
|
t.writeLine(t.prompt)
|
||||||
|
t.c.Write(t.outBuf)
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
lineIsPasted := t.pasteActive
|
||||||
|
|
||||||
|
for {
|
||||||
|
rest := t.remainder
|
||||||
|
lineOk := false
|
||||||
|
for !lineOk {
|
||||||
|
var key rune
|
||||||
|
key, rest = bytesToKey(rest, t.pasteActive)
|
||||||
|
if key == utf8.RuneError {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !t.pasteActive {
|
||||||
|
if key == keyCtrlD {
|
||||||
|
if len(t.line) == 0 {
|
||||||
|
return "", io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if key == keyPasteStart {
|
||||||
|
t.pasteActive = true
|
||||||
|
if len(t.line) == 0 {
|
||||||
|
lineIsPasted = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if key == keyPasteEnd {
|
||||||
|
t.pasteActive = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !t.pasteActive {
|
||||||
|
lineIsPasted = false
|
||||||
|
}
|
||||||
|
line, lineOk = t.handleKey(key)
|
||||||
|
}
|
||||||
|
if len(rest) > 0 {
|
||||||
|
n := copy(t.inBuf[:], rest)
|
||||||
|
t.remainder = t.inBuf[:n]
|
||||||
|
} else {
|
||||||
|
t.remainder = nil
|
||||||
|
}
|
||||||
|
t.c.Write(t.outBuf)
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
if lineOk {
|
||||||
|
if t.echo {
|
||||||
|
t.historyIndex = -1
|
||||||
|
t.history.Add(line)
|
||||||
|
}
|
||||||
|
if lineIsPasted {
|
||||||
|
err = ErrPasteIndicator
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// t.remainder is a slice at the beginning of t.inBuf
|
||||||
|
// containing a partial key sequence
|
||||||
|
readBuf := t.inBuf[len(t.remainder):]
|
||||||
|
var n int
|
||||||
|
|
||||||
|
t.lock.Unlock()
|
||||||
|
n, err = t.c.Read(readBuf)
|
||||||
|
t.lock.Lock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.remainder = t.inBuf[:n+len(t.remainder)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrompt sets the prompt to be used when reading subsequent lines.
|
||||||
|
func (t *Terminal) SetPrompt(prompt string) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
t.prompt = []rune(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
|
||||||
|
// Move cursor to column zero at the start of the line.
|
||||||
|
t.move(t.cursorY, 0, t.cursorX, 0)
|
||||||
|
t.cursorX, t.cursorY = 0, 0
|
||||||
|
t.clearLineToRight()
|
||||||
|
for t.cursorY < numPrevLines {
|
||||||
|
// Move down a line
|
||||||
|
t.move(0, 1, 0, 0)
|
||||||
|
t.cursorY++
|
||||||
|
t.clearLineToRight()
|
||||||
|
}
|
||||||
|
// Move back to beginning.
|
||||||
|
t.move(t.cursorY, 0, 0, 0)
|
||||||
|
t.cursorX, t.cursorY = 0, 0
|
||||||
|
|
||||||
|
t.queue(t.prompt)
|
||||||
|
t.advanceCursor(visualLength(t.prompt))
|
||||||
|
t.writeLine(t.line)
|
||||||
|
t.moveCursorToPos(t.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) SetSize(width, height int) error {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
if width == 0 {
|
||||||
|
width = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
oldWidth := t.termWidth
|
||||||
|
t.termWidth, t.termHeight = width, height
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case width == oldWidth:
|
||||||
|
// If the width didn't change then nothing else needs to be
|
||||||
|
// done.
|
||||||
|
return nil
|
||||||
|
case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0:
|
||||||
|
// If there is nothing on current line and no prompt printed,
|
||||||
|
// just do nothing
|
||||||
|
return nil
|
||||||
|
case width < oldWidth:
|
||||||
|
// Some terminals (e.g. xterm) will truncate lines that were
|
||||||
|
// too long when shinking. Others, (e.g. gnome-terminal) will
|
||||||
|
// attempt to wrap them. For the former, repainting t.maxLine
|
||||||
|
// works great, but that behaviour goes badly wrong in the case
|
||||||
|
// of the latter because they have doubled every full line.
|
||||||
|
|
||||||
|
// We assume that we are working on a terminal that wraps lines
|
||||||
|
// and adjust the cursor position based on every previous line
|
||||||
|
// wrapping and turning into two. This causes the prompt on
|
||||||
|
// xterms to move upwards, which isn't great, but it avoids a
|
||||||
|
// huge mess with gnome-terminal.
|
||||||
|
if t.cursorX >= t.termWidth {
|
||||||
|
t.cursorX = t.termWidth - 1
|
||||||
|
}
|
||||||
|
t.cursorY *= 2
|
||||||
|
t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
|
||||||
|
case width > oldWidth:
|
||||||
|
// If the terminal expands then our position calculations will
|
||||||
|
// be wrong in the future because we think the cursor is
|
||||||
|
// |t.pos| chars into the string, but there will be a gap at
|
||||||
|
// the end of any wrapped line.
|
||||||
|
//
|
||||||
|
// But the position will actually be correct until we move, so
|
||||||
|
// we can move back to the beginning and repaint everything.
|
||||||
|
t.clearAndRepaintLinePlusNPrevious(t.maxLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := t.c.Write(t.outBuf)
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type pasteIndicatorError struct{}
|
||||||
|
|
||||||
|
func (pasteIndicatorError) Error() string {
|
||||||
|
return "terminal: ErrPasteIndicator not correctly handled"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
|
||||||
|
// to valid line data. It indicates that bracketed paste mode is enabled and
|
||||||
|
// that the returned line consists only of pasted data. Programs may wish to
|
||||||
|
// interpret pasted data more literally than typed data.
|
||||||
|
var ErrPasteIndicator = pasteIndicatorError{}
|
||||||
|
|
||||||
|
// SetBracketedPasteMode requests that the terminal bracket paste operations
|
||||||
|
// with markers. Not all terminals support this but, if it is supported, then
|
||||||
|
// enabling this mode will stop any autocomplete callback from running due to
|
||||||
|
// pastes. Additionally, any lines that are completely pasted will be returned
|
||||||
|
// from ReadLine with the error set to ErrPasteIndicator.
|
||||||
|
func (t *Terminal) SetBracketedPasteMode(on bool) {
|
||||||
|
if on {
|
||||||
|
io.WriteString(t.c, "\x1b[?2004h")
|
||||||
|
} else {
|
||||||
|
io.WriteString(t.c, "\x1b[?2004l")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stRingBuffer is a ring buffer of strings.
|
||||||
|
type stRingBuffer struct {
|
||||||
|
// entries contains max elements.
|
||||||
|
entries []string
|
||||||
|
max int
|
||||||
|
// head contains the index of the element most recently added to the ring.
|
||||||
|
head int
|
||||||
|
// size contains the number of elements in the ring.
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stRingBuffer) Add(a string) {
|
||||||
|
if s.entries == nil {
|
||||||
|
const defaultNumEntries = 100
|
||||||
|
s.entries = make([]string, defaultNumEntries)
|
||||||
|
s.max = defaultNumEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
s.head = (s.head + 1) % s.max
|
||||||
|
s.entries[s.head] = a
|
||||||
|
if s.size < s.max {
|
||||||
|
s.size++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NthPreviousEntry returns the value passed to the nth previous call to Add.
|
||||||
|
// If n is zero then the immediately prior value is returned, if one, then the
|
||||||
|
// next most recent, and so on. If such an element doesn't exist then ok is
|
||||||
|
// false.
|
||||||
|
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
|
||||||
|
if n >= s.size {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
index := s.head - n
|
||||||
|
if index < 0 {
|
||||||
|
index += s.max
|
||||||
|
}
|
||||||
|
return s.entries[index], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPasswordLine reads from reader until it finds \n or io.EOF.
|
||||||
|
// The slice returned does not include the \n.
|
||||||
|
// readPasswordLine also ignores any \r it finds.
|
||||||
|
func readPasswordLine(reader io.Reader) ([]byte, error) {
|
||||||
|
var buf [1]byte
|
||||||
|
var ret []byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := reader.Read(buf[:])
|
||||||
|
if n > 0 {
|
||||||
|
switch buf[0] {
|
||||||
|
case '\n':
|
||||||
|
return ret, nil
|
||||||
|
case '\r':
|
||||||
|
// remove \r from passwords on Windows
|
||||||
|
default:
|
||||||
|
ret = append(ret, buf[0])
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF && len(ret) > 0 {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
vendor/golang.org/x/crypto/ssh/terminal/util.go
generated
vendored
Normal file
114
vendor/golang.org/x/crypto/ssh/terminal/util.go
generated
vendored
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package terminal // import "golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
termios unix.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := State{termios: *termios}
|
||||||
|
|
||||||
|
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||||
|
// the termios(3) manpage.
|
||||||
|
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||||
|
termios.Oflag &^= unix.OPOST
|
||||||
|
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||||
|
termios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||||
|
termios.Cflag |= unix.CS8
|
||||||
|
termios.Cc[unix.VMIN] = 1
|
||||||
|
termios.Cc[unix.VTIME] = 0
|
||||||
|
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oldState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{termios: *termios}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordReader is an io.Reader that reads from a specific file descriptor.
|
||||||
|
type passwordReader int
|
||||||
|
|
||||||
|
func (r passwordReader) Read(buf []byte) (int, error) {
|
||||||
|
return unix.Read(int(r), buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||||
|
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := *termios
|
||||||
|
newState.Lflag &^= unix.ECHO
|
||||||
|
newState.Lflag |= unix.ICANON | unix.ISIG
|
||||||
|
newState.Iflag |= unix.ICRNL
|
||||||
|
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer unix.IoctlSetTermios(fd, ioctlWriteTermios, termios)
|
||||||
|
|
||||||
|
return readPasswordLine(passwordReader(fd))
|
||||||
|
}
|
12
vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go
generated
vendored
Normal file
12
vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd netbsd openbsd
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TIOCGETA
|
||||||
|
const ioctlWriteTermios = unix.TIOCSETA
|
10
vendor/golang.org/x/crypto/ssh/terminal/util_linux.go
generated
vendored
Normal file
10
vendor/golang.org/x/crypto/ssh/terminal/util_linux.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TCGETS
|
||||||
|
const ioctlWriteTermios = unix.TCSETS
|
58
vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go
generated
vendored
Normal file
58
vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct{}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||||
|
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
124
vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go
generated
vendored
Normal file
124
vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go
generated
vendored
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build solaris
|
||||||
|
|
||||||
|
package terminal // import "golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
termios unix.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||||
|
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
// see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c
|
||||||
|
val, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
oldState := *val
|
||||||
|
|
||||||
|
newState := oldState
|
||||||
|
newState.Lflag &^= syscall.ECHO
|
||||||
|
newState.Lflag |= syscall.ICANON | syscall.ISIG
|
||||||
|
newState.Iflag |= syscall.ICRNL
|
||||||
|
err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState)
|
||||||
|
|
||||||
|
var buf [16]byte
|
||||||
|
var ret []byte
|
||||||
|
for {
|
||||||
|
n, err := syscall.Read(fd, buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if buf[n-1] == '\n' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
ret = append(ret, buf[:n]...)
|
||||||
|
if n < len(buf) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw puts the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
// see http://cr.illumos.org/~webrev/andy_js/1060/
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := State{termios: *termios}
|
||||||
|
|
||||||
|
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||||
|
termios.Oflag &^= unix.OPOST
|
||||||
|
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||||
|
termios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||||
|
termios.Cflag |= unix.CS8
|
||||||
|
termios.Cc[unix.VMIN] = 1
|
||||||
|
termios.Cc[unix.VTIME] = 0
|
||||||
|
|
||||||
|
if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oldState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, oldState *State) error {
|
||||||
|
return unix.IoctlSetTermios(fd, unix.TCSETS, &oldState.termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{termios: *termios}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row), nil
|
||||||
|
}
|
103
vendor/golang.org/x/crypto/ssh/terminal/util_windows.go
generated
vendored
Normal file
103
vendor/golang.org/x/crypto/ssh/terminal/util_windows.go
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
var st uint32
|
||||||
|
err := windows.GetConsoleMode(windows.Handle(fd), &st)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return windows.SetConsoleMode(windows.Handle(fd), state.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
var info windows.ConsoleScreenBufferInfo
|
||||||
|
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(info.Size.X), int(info.Size.Y), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||||
|
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
old := st
|
||||||
|
|
||||||
|
st &^= (windows.ENABLE_ECHO_INPUT)
|
||||||
|
st |= (windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer windows.SetConsoleMode(windows.Handle(fd), old)
|
||||||
|
|
||||||
|
var h windows.Handle
|
||||||
|
p, _ := windows.GetCurrentProcess()
|
||||||
|
if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f := os.NewFile(uintptr(h), "stdin")
|
||||||
|
defer f.Close()
|
||||||
|
return readPasswordLine(f)
|
||||||
|
}
|
Reference in New Issue
Block a user