mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-06 23:46:13 +02:00
Merge pull request #132 from jesseduffield/feature/project_restructure
Project Restructure Episode 1
This commit is contained in:
commit
c0a1f90604
1
.gitignore
vendored
1
.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
|
19
Gopkg.lock
generated
19
Gopkg.lock
generated
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
|
||||||
name = "github.com/davecgh/go-spew"
|
name = "github.com/Sirupsen/logrus"
|
||||||
packages = ["spew"]
|
packages = ["."]
|
||||||
pruneopts = "NUT"
|
pruneopts = "NUT"
|
||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||||
version = "v1.1.0"
|
version = "v1.0.6"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
|
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
|
||||||
@ -50,11 +50,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"
|
||||||
@ -151,7 +151,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 +170,7 @@
|
|||||||
"ssh",
|
"ssh",
|
||||||
"ssh/agent",
|
"ssh/agent",
|
||||||
"ssh/knownhosts",
|
"ssh/knownhosts",
|
||||||
|
"ssh/terminal",
|
||||||
]
|
]
|
||||||
pruneopts = "NUT"
|
pruneopts = "NUT"
|
||||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||||
@ -282,7 +283,7 @@
|
|||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
input-imports = [
|
input-imports = [
|
||||||
"github.com/davecgh/go-spew/spew",
|
"github.com/Sirupsen/logrus",
|
||||||
"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",
|
||||||
|
@ -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
|
|
||||||
}
|
|
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{
|
|
||||||
"← → ↑ ↓": "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 state.HasMergeConflicts {
|
|
||||||
optionsMap["a"] = "abort merge"
|
|
||||||
optionsMap["m"] = "resolve merge conflicts"
|
|
||||||
}
|
|
||||||
if gitFile == nil {
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
if gitFile.Tracked {
|
|
||||||
optionsMap["d"] = "checkout"
|
|
||||||
}
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
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, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
|
||||||
|
}
|
||||||
|
log.Out = 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.RunDirectCommand("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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (string, error) {
|
||||||
|
return c.OSCommand.RunCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashSave save stash
|
||||||
|
func (c *GitCommand) StashSave(message string) (string, error) {
|
||||||
|
output, err := c.OSCommand.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.RunCommand("git status"); err != nil {
|
||||||
|
fmt.Println(output)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBranchName branch name
|
||||||
|
func (c *GitCommand) GetBranchName() (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("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.RunDirectCommand("git rev-list @{u}..head --count")
|
||||||
|
if err != nil {
|
||||||
|
return "?", "?"
|
||||||
|
}
|
||||||
|
pullableCount, err := c.OSCommand.RunDirectCommand("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.RunDirectCommand("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) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git commit --allow-empty --amend -m \"" + name + "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fetch git repo
|
||||||
|
func (c *GitCommand) Fetch() (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetToCommit reset to commit
|
||||||
|
func (c *GitCommand) ResetToCommit(sha string) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git reset " + sha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBranch create new branch
|
||||||
|
func (c *GitCommand) NewBranch(name string) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git checkout -b " + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBranch delete branch
|
||||||
|
func (c *GitCommand) DeleteBranch(branch string) (string, error) {
|
||||||
|
return c.OSCommand.RunCommand("git branch -d " + branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListStash list stash
|
||||||
|
func (c *GitCommand) ListStash() (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git stash list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merge
|
||||||
|
func (c *GitCommand) Merge(branchName string) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git merge --no-edit " + branchName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortMerge abort merge
|
||||||
|
func (c *GitCommand) AbortMerge() (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("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 \"" + message + "\""
|
||||||
|
if c.UsingGpg() {
|
||||||
|
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)
|
||||||
|
}
|
||||||
|
// TODO: make these runDirectCommand functions just return an error
|
||||||
|
_, err := c.OSCommand.RunDirectCommand(command)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull pull from repo
|
||||||
|
func (c *GitCommand) Pull() (string, error) {
|
||||||
|
return c.OSCommand.RunCommand("git pull --no-edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push push to a branch
|
||||||
|
func (c *GitCommand) Push(branchName string) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("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) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + 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) (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.RunDirectCommand(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.RunDirectCommand("git branch -d " + shaValue)
|
||||||
|
ret += output
|
||||||
|
output, _ = c.OSCommand.RunDirectCommand("git checkout " + branchName)
|
||||||
|
ret += output
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatFile obtain the contents of a file
|
||||||
|
func (c *GitCommand) CatFile(file string) (string, error) {
|
||||||
|
return c.OSCommand.RunDirectCommand("cat " + file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StageFile stages a file
|
||||||
|
func (c *GitCommand) StageFile(file string) error {
|
||||||
|
_, err := c.OSCommand.RunCommand("git add " + file)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "
|
||||||
|
}
|
||||||
|
_, err := c.OSCommand.RunCommand(command + file)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitStatus returns the plaintext short status of the repo
|
||||||
|
func (c *GitCommand) GitStatus() (string, error) {
|
||||||
|
return c.OSCommand.RunCommand("git status --untracked-files=all --short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInMergeState states whether we are still mid-merge
|
||||||
|
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||||
|
output, err := c.OSCommand.RunCommand("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 {
|
||||||
|
_, err := c.OSCommand.RunCommand("rm -rf ./" + file.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if the file is tracked, we assume you want to just check it out
|
||||||
|
_, err := c.OSCommand.RunCommand("git checkout " + file.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout checks out a branch, with --force if you set the force arg to true
|
||||||
|
func (c *GitCommand) Checkout(branch string, force bool) (string, 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.RunCommand("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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.RunDirectCommand("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 := ""
|
||||||
|
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, _ := c.OSCommand.RunCommand(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
|
||||||
|
}
|
157
pkg/commands/os.go
Normal file
157
pkg/commands/os.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand wrapper around commands
|
||||||
|
func (c *OSCommand) RunCommand(command string) (string, error) {
|
||||||
|
c.Log.WithField("command", command).Info("RunCommand")
|
||||||
|
splitCmd := strings.Split(command, " ")
|
||||||
|
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
|
||||||
|
return sanitisedCommandOutput(cmdOut, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunDirectCommand wrapper around direct commands
|
||||||
|
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||||
|
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||||
|
|
||||||
|
cmdOut, err := exec.
|
||||||
|
Command(c.Platform.shell, c.Platform.shellArg, command).
|
||||||
|
CombinedOutput()
|
||||||
|
return sanitisedCommandOutput(cmdOut, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||||
|
outputString := string(output)
|
||||||
|
if outputString == "" && err != nil {
|
||||||
|
return err.Error(), err
|
||||||
|
}
|
||||||
|
return outputString, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 out, _ := c.RunCommand("which " + name); out != "exit status 1" {
|
||||||
|
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) {
|
||||||
|
_, err := c.RunCommand("code -r " + filename)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SublimeOpenFile opens the filein sublime
|
||||||
|
// may be deprecated in the future
|
||||||
|
func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) {
|
||||||
|
_, err := c.RunCommand("subl " + filename)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
}
|
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,61 @@ 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, _ := b.GitCommand.OSCommand.RunDirectCommand("git symbolic-ref --short HEAD")
|
||||||
return Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
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.RunDirectCommand("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 +83,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 +92,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 +112,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 output, err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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 output, err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
389
pkg/gui/files_panel.go
Normal file
389
pkg/gui/files_panel.go
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
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(files []commands.File) []commands.File {
|
||||||
|
result := make([]commands.File, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.HasStagedChanges {
|
||||||
|
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(gui.State.Files)) == 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(gui.State.Files)) == 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 output, err := gui.GitCommand.Pull(); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
} 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 output, err := gui.GitCommand.Push(branchName); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
} 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 {
|
||||||
|
output, err := gui.GitCommand.AbortMerge()
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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,53 +1,84 @@
|
|||||||
package main
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
||||||
// "io"
|
// "io"
|
||||||
// "io/ioutil"
|
// "io/ioutil"
|
||||||
|
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"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 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),
|
||||||
}
|
|
||||||
|
|
||||||
var state = stateType{
|
|
||||||
GitFiles: make([]GitFile, 0),
|
|
||||||
PreviousView: "files",
|
PreviousView: "files",
|
||||||
Commits: make([]Commit, 0),
|
Commits: make([]commands.Commit, 0),
|
||||||
StashEntries: make([]StashEntry, 0),
|
StashEntries: make([]commands.StashEntry, 0),
|
||||||
ConflictIndex: 0,
|
ConflictIndex: 0,
|
||||||
ConflictTop: true,
|
ConflictTop: true,
|
||||||
Conflicts: make([]conflict, 0),
|
Conflicts: make([]commands.Conflict, 0),
|
||||||
EditHistory: stack.New(),
|
EditHistory: stack.New(),
|
||||||
Platform: getPlatform(),
|
Platform: getPlatform(),
|
||||||
|
Version: "test version", // TODO: send version in
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Gui{
|
||||||
|
Log: log,
|
||||||
|
GitCommand: gitCommand,
|
||||||
|
OSCommand: oSCommand,
|
||||||
|
Version: version,
|
||||||
|
State: initialState,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type platform struct {
|
type platform struct {
|
||||||
@ -76,7 +107,7 @@ func getPlatform() platform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +116,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 +125,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 +137,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()
|
||||||
@ -195,7 +226,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
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,7 +234,7 @@ 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 {
|
||||||
@ -216,47 +247,47 @@ func layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +295,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,75 +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.KeyCtrlU, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
|
||||||
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
|
||||||
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
|
||||||
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
|
||||||
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
|
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles},
|
||||||
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
|
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles},
|
||||||
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
|
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
|
||||||
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
|
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress},
|
||||||
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress},
|
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
|
||||||
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
|
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress},
|
||||||
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
|
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove},
|
||||||
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
|
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge},
|
||||||
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
|
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit},
|
||||||
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
|
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen},
|
||||||
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
|
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen},
|
||||||
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
|
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen},
|
||||||
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
|
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile},
|
||||||
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
|
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles},
|
||||||
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
|
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave},
|
||||||
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
|
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge},
|
||||||
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
|
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch},
|
||||||
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
|
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard},
|
||||||
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
|
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge},
|
||||||
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
|
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk},
|
||||||
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
|
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
|
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot},
|
||||||
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
|
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress},
|
||||||
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
|
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName},
|
||||||
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
|
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
|
||||||
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
|
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
|
||||||
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch},
|
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
|
||||||
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
|
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
|
||||||
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
|
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
|
||||||
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
|
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},
|
||||||
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
|
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit},
|
||||||
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
|
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup},
|
||||||
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
|
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply},
|
||||||
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
|
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop},
|
||||||
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
|
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm},
|
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose},
|
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage},
|
{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)
|
||||||
|
}
|
94
pkg/gui/stash_panel.go
Normal file
94
pkg/gui/stash_panel.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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 output, err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
gui.refreshStashEntries(g)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
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
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
15
vendor/github.com/davecgh/go-spew/LICENSE
generated
vendored
15
vendor/github.com/davecgh/go-spew/LICENSE
generated
vendored
@ -1,15 +0,0 @@
|
|||||||
ISC License
|
|
||||||
|
|
||||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
|
||||||
copyright notice and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
152
vendor/github.com/davecgh/go-spew/spew/bypass.go
generated
vendored
152
vendor/github.com/davecgh/go-spew/spew/bypass.go
generated
vendored
@ -1,152 +0,0 @@
|
|||||||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
|
||||||
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
|
||||||
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
|
||||||
// tag is deprecated and thus should not be used.
|
|
||||||
// +build !js,!appengine,!safe,!disableunsafe
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// UnsafeDisabled is a build-time constant which specifies whether or
|
|
||||||
// not access to the unsafe package is available.
|
|
||||||
UnsafeDisabled = false
|
|
||||||
|
|
||||||
// ptrSize is the size of a pointer on the current arch.
|
|
||||||
ptrSize = unsafe.Sizeof((*byte)(nil))
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
|
||||||
// internal reflect.Value fields. These values are valid before golang
|
|
||||||
// commit ecccf07e7f9d which changed the format. The are also valid
|
|
||||||
// after commit 82f48826c6c7 which changed the format again to mirror
|
|
||||||
// the original format. Code in the init function updates these offsets
|
|
||||||
// as necessary.
|
|
||||||
offsetPtr = uintptr(ptrSize)
|
|
||||||
offsetScalar = uintptr(0)
|
|
||||||
offsetFlag = uintptr(ptrSize * 2)
|
|
||||||
|
|
||||||
// flagKindWidth and flagKindShift indicate various bits that the
|
|
||||||
// reflect package uses internally to track kind information.
|
|
||||||
//
|
|
||||||
// flagRO indicates whether or not the value field of a reflect.Value is
|
|
||||||
// read-only.
|
|
||||||
//
|
|
||||||
// flagIndir indicates whether the value field of a reflect.Value is
|
|
||||||
// the actual data or a pointer to the data.
|
|
||||||
//
|
|
||||||
// These values are valid before golang commit 90a7c3c86944 which
|
|
||||||
// changed their positions. Code in the init function updates these
|
|
||||||
// flags as necessary.
|
|
||||||
flagKindWidth = uintptr(5)
|
|
||||||
flagKindShift = uintptr(flagKindWidth - 1)
|
|
||||||
flagRO = uintptr(1 << 0)
|
|
||||||
flagIndir = uintptr(1 << 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Older versions of reflect.Value stored small integers directly in the
|
|
||||||
// ptr field (which is named val in the older versions). Versions
|
|
||||||
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
|
||||||
// scalar for this purpose which unfortunately came before the flag
|
|
||||||
// field, so the offset of the flag field is different for those
|
|
||||||
// versions.
|
|
||||||
//
|
|
||||||
// This code constructs a new reflect.Value from a known small integer
|
|
||||||
// and checks if the size of the reflect.Value struct indicates it has
|
|
||||||
// the scalar field. When it does, the offsets are updated accordingly.
|
|
||||||
vv := reflect.ValueOf(0xf00)
|
|
||||||
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
|
||||||
offsetScalar = ptrSize * 2
|
|
||||||
offsetFlag = ptrSize * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit 90a7c3c86944 changed the flag positions such that the low
|
|
||||||
// order bits are the kind. This code extracts the kind from the flags
|
|
||||||
// field and ensures it's the correct type. When it's not, the flag
|
|
||||||
// order has been changed to the newer format, so the flags are updated
|
|
||||||
// accordingly.
|
|
||||||
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
|
||||||
upfv := *(*uintptr)(upf)
|
|
||||||
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
|
||||||
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
|
||||||
flagKindShift = 0
|
|
||||||
flagRO = 1 << 5
|
|
||||||
flagIndir = 1 << 6
|
|
||||||
|
|
||||||
// Commit adf9b30e5594 modified the flags to separate the
|
|
||||||
// flagRO flag into two bits which specifies whether or not the
|
|
||||||
// field is embedded. This causes flagIndir to move over a bit
|
|
||||||
// and means that flagRO is the combination of either of the
|
|
||||||
// original flagRO bit and the new bit.
|
|
||||||
//
|
|
||||||
// This code detects the change by extracting what used to be
|
|
||||||
// the indirect bit to ensure it's set. When it's not, the flag
|
|
||||||
// order has been changed to the newer format, so the flags are
|
|
||||||
// updated accordingly.
|
|
||||||
if upfv&flagIndir == 0 {
|
|
||||||
flagRO = 3 << 5
|
|
||||||
flagIndir = 1 << 7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
|
||||||
// the typical safety restrictions preventing access to unaddressable and
|
|
||||||
// unexported data. It works by digging the raw pointer to the underlying
|
|
||||||
// value out of the protected value and generating a new unprotected (unsafe)
|
|
||||||
// reflect.Value to it.
|
|
||||||
//
|
|
||||||
// This allows us to check for implementations of the Stringer and error
|
|
||||||
// interfaces to be used for pretty printing ordinarily unaddressable and
|
|
||||||
// inaccessible values such as unexported struct fields.
|
|
||||||
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
|
||||||
indirects := 1
|
|
||||||
vt := v.Type()
|
|
||||||
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
|
||||||
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
|
||||||
if rvf&flagIndir != 0 {
|
|
||||||
vt = reflect.PtrTo(v.Type())
|
|
||||||
indirects++
|
|
||||||
} else if offsetScalar != 0 {
|
|
||||||
// The value is in the scalar field when it's not one of the
|
|
||||||
// reference types.
|
|
||||||
switch vt.Kind() {
|
|
||||||
case reflect.Uintptr:
|
|
||||||
case reflect.Chan:
|
|
||||||
case reflect.Func:
|
|
||||||
case reflect.Map:
|
|
||||||
case reflect.Ptr:
|
|
||||||
case reflect.UnsafePointer:
|
|
||||||
default:
|
|
||||||
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
|
||||||
offsetScalar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pv := reflect.NewAt(vt, upv)
|
|
||||||
rv = pv
|
|
||||||
for i := 0; i < indirects; i++ {
|
|
||||||
rv = rv.Elem()
|
|
||||||
}
|
|
||||||
return rv
|
|
||||||
}
|
|
38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
generated
vendored
38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
generated
vendored
@ -1,38 +0,0 @@
|
|||||||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
|
||||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
|
||||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
|
||||||
// tag is deprecated and thus should not be used.
|
|
||||||
// +build js appengine safe disableunsafe
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import "reflect"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// UnsafeDisabled is a build-time constant which specifies whether or
|
|
||||||
// not access to the unsafe package is available.
|
|
||||||
UnsafeDisabled = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// unsafeReflectValue typically converts the passed reflect.Value into a one
|
|
||||||
// that bypasses the typical safety restrictions preventing access to
|
|
||||||
// unaddressable and unexported data. However, doing this relies on access to
|
|
||||||
// the unsafe package. This is a stub version which simply returns the passed
|
|
||||||
// reflect.Value when the unsafe package is not available.
|
|
||||||
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
|
||||||
return v
|
|
||||||
}
|
|
341
vendor/github.com/davecgh/go-spew/spew/common.go
generated
vendored
341
vendor/github.com/davecgh/go-spew/spew/common.go
generated
vendored
@ -1,341 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Some constants in the form of bytes to avoid string overhead. This mirrors
|
|
||||||
// the technique used in the fmt package.
|
|
||||||
var (
|
|
||||||
panicBytes = []byte("(PANIC=")
|
|
||||||
plusBytes = []byte("+")
|
|
||||||
iBytes = []byte("i")
|
|
||||||
trueBytes = []byte("true")
|
|
||||||
falseBytes = []byte("false")
|
|
||||||
interfaceBytes = []byte("(interface {})")
|
|
||||||
commaNewlineBytes = []byte(",\n")
|
|
||||||
newlineBytes = []byte("\n")
|
|
||||||
openBraceBytes = []byte("{")
|
|
||||||
openBraceNewlineBytes = []byte("{\n")
|
|
||||||
closeBraceBytes = []byte("}")
|
|
||||||
asteriskBytes = []byte("*")
|
|
||||||
colonBytes = []byte(":")
|
|
||||||
colonSpaceBytes = []byte(": ")
|
|
||||||
openParenBytes = []byte("(")
|
|
||||||
closeParenBytes = []byte(")")
|
|
||||||
spaceBytes = []byte(" ")
|
|
||||||
pointerChainBytes = []byte("->")
|
|
||||||
nilAngleBytes = []byte("<nil>")
|
|
||||||
maxNewlineBytes = []byte("<max depth reached>\n")
|
|
||||||
maxShortBytes = []byte("<max>")
|
|
||||||
circularBytes = []byte("<already shown>")
|
|
||||||
circularShortBytes = []byte("<shown>")
|
|
||||||
invalidAngleBytes = []byte("<invalid>")
|
|
||||||
openBracketBytes = []byte("[")
|
|
||||||
closeBracketBytes = []byte("]")
|
|
||||||
percentBytes = []byte("%")
|
|
||||||
precisionBytes = []byte(".")
|
|
||||||
openAngleBytes = []byte("<")
|
|
||||||
closeAngleBytes = []byte(">")
|
|
||||||
openMapBytes = []byte("map[")
|
|
||||||
closeMapBytes = []byte("]")
|
|
||||||
lenEqualsBytes = []byte("len=")
|
|
||||||
capEqualsBytes = []byte("cap=")
|
|
||||||
)
|
|
||||||
|
|
||||||
// hexDigits is used to map a decimal value to a hex digit.
|
|
||||||
var hexDigits = "0123456789abcdef"
|
|
||||||
|
|
||||||
// catchPanic handles any panics that might occur during the handleMethods
|
|
||||||
// calls.
|
|
||||||
func catchPanic(w io.Writer, v reflect.Value) {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
w.Write(panicBytes)
|
|
||||||
fmt.Fprintf(w, "%v", err)
|
|
||||||
w.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMethods attempts to call the Error and String methods on the underlying
|
|
||||||
// type the passed reflect.Value represents and outputes the result to Writer w.
|
|
||||||
//
|
|
||||||
// It handles panics in any called methods by catching and displaying the error
|
|
||||||
// as the formatted value.
|
|
||||||
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
|
|
||||||
// We need an interface to check if the type implements the error or
|
|
||||||
// Stringer interface. However, the reflect package won't give us an
|
|
||||||
// interface on certain things like unexported struct fields in order
|
|
||||||
// to enforce visibility rules. We use unsafe, when it's available,
|
|
||||||
// to bypass these restrictions since this package does not mutate the
|
|
||||||
// values.
|
|
||||||
if !v.CanInterface() {
|
|
||||||
if UnsafeDisabled {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
v = unsafeReflectValue(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose whether or not to do error and Stringer interface lookups against
|
|
||||||
// the base type or a pointer to the base type depending on settings.
|
|
||||||
// Technically calling one of these methods with a pointer receiver can
|
|
||||||
// mutate the value, however, types which choose to satisify an error or
|
|
||||||
// Stringer interface with a pointer receiver should not be mutating their
|
|
||||||
// state inside these interface methods.
|
|
||||||
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
|
|
||||||
v = unsafeReflectValue(v)
|
|
||||||
}
|
|
||||||
if v.CanAddr() {
|
|
||||||
v = v.Addr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it an error or Stringer?
|
|
||||||
switch iface := v.Interface().(type) {
|
|
||||||
case error:
|
|
||||||
defer catchPanic(w, v)
|
|
||||||
if cs.ContinueOnMethod {
|
|
||||||
w.Write(openParenBytes)
|
|
||||||
w.Write([]byte(iface.Error()))
|
|
||||||
w.Write(closeParenBytes)
|
|
||||||
w.Write(spaceBytes)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(iface.Error()))
|
|
||||||
return true
|
|
||||||
|
|
||||||
case fmt.Stringer:
|
|
||||||
defer catchPanic(w, v)
|
|
||||||
if cs.ContinueOnMethod {
|
|
||||||
w.Write(openParenBytes)
|
|
||||||
w.Write([]byte(iface.String()))
|
|
||||||
w.Write(closeParenBytes)
|
|
||||||
w.Write(spaceBytes)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
w.Write([]byte(iface.String()))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// printBool outputs a boolean value as true or false to Writer w.
|
|
||||||
func printBool(w io.Writer, val bool) {
|
|
||||||
if val {
|
|
||||||
w.Write(trueBytes)
|
|
||||||
} else {
|
|
||||||
w.Write(falseBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// printInt outputs a signed integer value to Writer w.
|
|
||||||
func printInt(w io.Writer, val int64, base int) {
|
|
||||||
w.Write([]byte(strconv.FormatInt(val, base)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// printUint outputs an unsigned integer value to Writer w.
|
|
||||||
func printUint(w io.Writer, val uint64, base int) {
|
|
||||||
w.Write([]byte(strconv.FormatUint(val, base)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// printFloat outputs a floating point value using the specified precision,
|
|
||||||
// which is expected to be 32 or 64bit, to Writer w.
|
|
||||||
func printFloat(w io.Writer, val float64, precision int) {
|
|
||||||
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// printComplex outputs a complex value using the specified float precision
|
|
||||||
// for the real and imaginary parts to Writer w.
|
|
||||||
func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
|
||||||
r := real(c)
|
|
||||||
w.Write(openParenBytes)
|
|
||||||
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
|
|
||||||
i := imag(c)
|
|
||||||
if i >= 0 {
|
|
||||||
w.Write(plusBytes)
|
|
||||||
}
|
|
||||||
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
|
|
||||||
w.Write(iBytes)
|
|
||||||
w.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
|
||||||
// prefix to Writer w.
|
|
||||||
func printHexPtr(w io.Writer, p uintptr) {
|
|
||||||
// Null pointer.
|
|
||||||
num := uint64(p)
|
|
||||||
if num == 0 {
|
|
||||||
w.Write(nilAngleBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
|
|
||||||
buf := make([]byte, 18)
|
|
||||||
|
|
||||||
// It's simpler to construct the hex string right to left.
|
|
||||||
base := uint64(16)
|
|
||||||
i := len(buf) - 1
|
|
||||||
for num >= base {
|
|
||||||
buf[i] = hexDigits[num%base]
|
|
||||||
num /= base
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
buf[i] = hexDigits[num]
|
|
||||||
|
|
||||||
// Add '0x' prefix.
|
|
||||||
i--
|
|
||||||
buf[i] = 'x'
|
|
||||||
i--
|
|
||||||
buf[i] = '0'
|
|
||||||
|
|
||||||
// Strip unused leading bytes.
|
|
||||||
buf = buf[i:]
|
|
||||||
w.Write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
|
|
||||||
// elements to be sorted.
|
|
||||||
type valuesSorter struct {
|
|
||||||
values []reflect.Value
|
|
||||||
strings []string // either nil or same len and values
|
|
||||||
cs *ConfigState
|
|
||||||
}
|
|
||||||
|
|
||||||
// newValuesSorter initializes a valuesSorter instance, which holds a set of
|
|
||||||
// surrogate keys on which the data should be sorted. It uses flags in
|
|
||||||
// ConfigState to decide if and how to populate those surrogate keys.
|
|
||||||
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
|
|
||||||
vs := &valuesSorter{values: values, cs: cs}
|
|
||||||
if canSortSimply(vs.values[0].Kind()) {
|
|
||||||
return vs
|
|
||||||
}
|
|
||||||
if !cs.DisableMethods {
|
|
||||||
vs.strings = make([]string, len(values))
|
|
||||||
for i := range vs.values {
|
|
||||||
b := bytes.Buffer{}
|
|
||||||
if !handleMethods(cs, &b, vs.values[i]) {
|
|
||||||
vs.strings = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
vs.strings[i] = b.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if vs.strings == nil && cs.SpewKeys {
|
|
||||||
vs.strings = make([]string, len(values))
|
|
||||||
for i := range vs.values {
|
|
||||||
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vs
|
|
||||||
}
|
|
||||||
|
|
||||||
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
|
|
||||||
// directly, or whether it should be considered for sorting by surrogate keys
|
|
||||||
// (if the ConfigState allows it).
|
|
||||||
func canSortSimply(kind reflect.Kind) bool {
|
|
||||||
// This switch parallels valueSortLess, except for the default case.
|
|
||||||
switch kind {
|
|
||||||
case reflect.Bool:
|
|
||||||
return true
|
|
||||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
||||||
return true
|
|
||||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
||||||
return true
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return true
|
|
||||||
case reflect.String:
|
|
||||||
return true
|
|
||||||
case reflect.Uintptr:
|
|
||||||
return true
|
|
||||||
case reflect.Array:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of values in the slice. It is part of the
|
|
||||||
// sort.Interface implementation.
|
|
||||||
func (s *valuesSorter) Len() int {
|
|
||||||
return len(s.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap swaps the values at the passed indices. It is part of the
|
|
||||||
// sort.Interface implementation.
|
|
||||||
func (s *valuesSorter) Swap(i, j int) {
|
|
||||||
s.values[i], s.values[j] = s.values[j], s.values[i]
|
|
||||||
if s.strings != nil {
|
|
||||||
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// valueSortLess returns whether the first value should sort before the second
|
|
||||||
// value. It is used by valueSorter.Less as part of the sort.Interface
|
|
||||||
// implementation.
|
|
||||||
func valueSortLess(a, b reflect.Value) bool {
|
|
||||||
switch a.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
return !a.Bool() && b.Bool()
|
|
||||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
||||||
return a.Int() < b.Int()
|
|
||||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
||||||
return a.Uint() < b.Uint()
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return a.Float() < b.Float()
|
|
||||||
case reflect.String:
|
|
||||||
return a.String() < b.String()
|
|
||||||
case reflect.Uintptr:
|
|
||||||
return a.Uint() < b.Uint()
|
|
||||||
case reflect.Array:
|
|
||||||
// Compare the contents of both arrays.
|
|
||||||
l := a.Len()
|
|
||||||
for i := 0; i < l; i++ {
|
|
||||||
av := a.Index(i)
|
|
||||||
bv := b.Index(i)
|
|
||||||
if av.Interface() == bv.Interface() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return valueSortLess(av, bv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a.String() < b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Less returns whether the value at index i should sort before the
|
|
||||||
// value at index j. It is part of the sort.Interface implementation.
|
|
||||||
func (s *valuesSorter) Less(i, j int) bool {
|
|
||||||
if s.strings == nil {
|
|
||||||
return valueSortLess(s.values[i], s.values[j])
|
|
||||||
}
|
|
||||||
return s.strings[i] < s.strings[j]
|
|
||||||
}
|
|
||||||
|
|
||||||
// sortValues is a sort function that handles both native types and any type that
|
|
||||||
// can be converted to error or Stringer. Other inputs are sorted according to
|
|
||||||
// their Value.String() value to ensure display stability.
|
|
||||||
func sortValues(values []reflect.Value, cs *ConfigState) {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sort.Sort(newValuesSorter(values, cs))
|
|
||||||
}
|
|
306
vendor/github.com/davecgh/go-spew/spew/config.go
generated
vendored
306
vendor/github.com/davecgh/go-spew/spew/config.go
generated
vendored
@ -1,306 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfigState houses the configuration options used by spew to format and
|
|
||||||
// display values. There is a global instance, Config, that is used to control
|
|
||||||
// all top-level Formatter and Dump functionality. Each ConfigState instance
|
|
||||||
// provides methods equivalent to the top-level functions.
|
|
||||||
//
|
|
||||||
// The zero value for ConfigState provides no indentation. You would typically
|
|
||||||
// want to set it to a space or a tab.
|
|
||||||
//
|
|
||||||
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
|
|
||||||
// with default settings. See the documentation of NewDefaultConfig for default
|
|
||||||
// values.
|
|
||||||
type ConfigState struct {
|
|
||||||
// Indent specifies the string to use for each indentation level. The
|
|
||||||
// global config instance that all top-level functions use set this to a
|
|
||||||
// single space by default. If you would like more indentation, you might
|
|
||||||
// set this to a tab with "\t" or perhaps two spaces with " ".
|
|
||||||
Indent string
|
|
||||||
|
|
||||||
// MaxDepth controls the maximum number of levels to descend into nested
|
|
||||||
// data structures. The default, 0, means there is no limit.
|
|
||||||
//
|
|
||||||
// NOTE: Circular data structures are properly detected, so it is not
|
|
||||||
// necessary to set this value unless you specifically want to limit deeply
|
|
||||||
// nested data structures.
|
|
||||||
MaxDepth int
|
|
||||||
|
|
||||||
// DisableMethods specifies whether or not error and Stringer interfaces are
|
|
||||||
// invoked for types that implement them.
|
|
||||||
DisableMethods bool
|
|
||||||
|
|
||||||
// DisablePointerMethods specifies whether or not to check for and invoke
|
|
||||||
// error and Stringer interfaces on types which only accept a pointer
|
|
||||||
// receiver when the current type is not a pointer.
|
|
||||||
//
|
|
||||||
// NOTE: This might be an unsafe action since calling one of these methods
|
|
||||||
// with a pointer receiver could technically mutate the value, however,
|
|
||||||
// in practice, types which choose to satisify an error or Stringer
|
|
||||||
// interface with a pointer receiver should not be mutating their state
|
|
||||||
// inside these interface methods. As a result, this option relies on
|
|
||||||
// access to the unsafe package, so it will not have any effect when
|
|
||||||
// running in environments without access to the unsafe package such as
|
|
||||||
// Google App Engine or with the "safe" build tag specified.
|
|
||||||
DisablePointerMethods bool
|
|
||||||
|
|
||||||
// DisablePointerAddresses specifies whether to disable the printing of
|
|
||||||
// pointer addresses. This is useful when diffing data structures in tests.
|
|
||||||
DisablePointerAddresses bool
|
|
||||||
|
|
||||||
// DisableCapacities specifies whether to disable the printing of capacities
|
|
||||||
// for arrays, slices, maps and channels. This is useful when diffing
|
|
||||||
// data structures in tests.
|
|
||||||
DisableCapacities bool
|
|
||||||
|
|
||||||
// ContinueOnMethod specifies whether or not recursion should continue once
|
|
||||||
// a custom error or Stringer interface is invoked. The default, false,
|
|
||||||
// means it will print the results of invoking the custom error or Stringer
|
|
||||||
// interface and return immediately instead of continuing to recurse into
|
|
||||||
// the internals of the data type.
|
|
||||||
//
|
|
||||||
// NOTE: This flag does not have any effect if method invocation is disabled
|
|
||||||
// via the DisableMethods or DisablePointerMethods options.
|
|
||||||
ContinueOnMethod bool
|
|
||||||
|
|
||||||
// SortKeys specifies map keys should be sorted before being printed. Use
|
|
||||||
// this to have a more deterministic, diffable output. Note that only
|
|
||||||
// native types (bool, int, uint, floats, uintptr and string) and types
|
|
||||||
// that support the error or Stringer interfaces (if methods are
|
|
||||||
// enabled) are supported, with other types sorted according to the
|
|
||||||
// reflect.Value.String() output which guarantees display stability.
|
|
||||||
SortKeys bool
|
|
||||||
|
|
||||||
// SpewKeys specifies that, as a last resort attempt, map keys should
|
|
||||||
// be spewed to strings and sorted by those strings. This is only
|
|
||||||
// considered if SortKeys is true.
|
|
||||||
SpewKeys bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config is the active configuration of the top-level functions.
|
|
||||||
// The configuration can be changed by modifying the contents of spew.Config.
|
|
||||||
var Config = ConfigState{Indent: " "}
|
|
||||||
|
|
||||||
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the formatted string as a value that satisfies error. See NewFormatter
|
|
||||||
// for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
|
|
||||||
return fmt.Errorf(format, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprint(w, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprintf(w, format, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprintln(w, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Print(c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Printf(format, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Println(c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Sprint(a ...interface{}) string {
|
|
||||||
return fmt.Sprint(c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
|
||||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
|
||||||
// the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
|
|
||||||
return fmt.Sprintf(format, c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
|
||||||
// were passed with a Formatter interface returned by c.NewFormatter. It
|
|
||||||
// returns the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
|
|
||||||
func (c *ConfigState) Sprintln(a ...interface{}) string {
|
|
||||||
return fmt.Sprintln(c.convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
|
||||||
interface. As a result, it integrates cleanly with standard fmt package
|
|
||||||
printing functions. The formatter is useful for inline printing of smaller data
|
|
||||||
types similar to the standard %v format specifier.
|
|
||||||
|
|
||||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
|
||||||
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
|
|
||||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
|
||||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
|
||||||
the width and precision arguments (however they will still work on the format
|
|
||||||
specifiers not handled by the custom formatter).
|
|
||||||
|
|
||||||
Typically this function shouldn't be called directly. It is much easier to make
|
|
||||||
use of the custom formatter by calling one of the convenience functions such as
|
|
||||||
c.Printf, c.Println, or c.Printf.
|
|
||||||
*/
|
|
||||||
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
|
|
||||||
return newFormatter(c, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
|
||||||
// exactly the same as Dump.
|
|
||||||
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
|
|
||||||
fdump(c, w, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Dump displays the passed parameters to standard out with newlines, customizable
|
|
||||||
indentation, and additional debug information such as complete types and all
|
|
||||||
pointer addresses used to indirect to the final value. It provides the
|
|
||||||
following features over the built-in printing facilities provided by the fmt
|
|
||||||
package:
|
|
||||||
|
|
||||||
* Pointers are dereferenced and followed
|
|
||||||
* Circular data structures are detected and handled properly
|
|
||||||
* Custom Stringer/error interfaces are optionally invoked, including
|
|
||||||
on unexported types
|
|
||||||
* Custom types which only implement the Stringer/error interfaces via
|
|
||||||
a pointer receiver are optionally invoked when passing non-pointer
|
|
||||||
variables
|
|
||||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
|
||||||
includes offsets, byte values in hex, and ASCII output
|
|
||||||
|
|
||||||
The configuration options are controlled by modifying the public members
|
|
||||||
of c. See ConfigState for options documentation.
|
|
||||||
|
|
||||||
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
|
||||||
get the formatted result as a string.
|
|
||||||
*/
|
|
||||||
func (c *ConfigState) Dump(a ...interface{}) {
|
|
||||||
fdump(c, os.Stdout, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sdump returns a string with the passed arguments formatted exactly the same
|
|
||||||
// as Dump.
|
|
||||||
func (c *ConfigState) Sdump(a ...interface{}) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
fdump(c, &buf, a...)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertArgs accepts a slice of arguments and returns a slice of the same
|
|
||||||
// length with each argument converted to a spew Formatter interface using
|
|
||||||
// the ConfigState associated with s.
|
|
||||||
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
|
|
||||||
formatters = make([]interface{}, len(args))
|
|
||||||
for index, arg := range args {
|
|
||||||
formatters[index] = newFormatter(c, arg)
|
|
||||||
}
|
|
||||||
return formatters
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDefaultConfig returns a ConfigState with the following default settings.
|
|
||||||
//
|
|
||||||
// Indent: " "
|
|
||||||
// MaxDepth: 0
|
|
||||||
// DisableMethods: false
|
|
||||||
// DisablePointerMethods: false
|
|
||||||
// ContinueOnMethod: false
|
|
||||||
// SortKeys: false
|
|
||||||
func NewDefaultConfig() *ConfigState {
|
|
||||||
return &ConfigState{Indent: " "}
|
|
||||||
}
|
|
211
vendor/github.com/davecgh/go-spew/spew/doc.go
generated
vendored
211
vendor/github.com/davecgh/go-spew/spew/doc.go
generated
vendored
@ -1,211 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Package spew implements a deep pretty printer for Go data structures to aid in
|
|
||||||
debugging.
|
|
||||||
|
|
||||||
A quick overview of the additional features spew provides over the built-in
|
|
||||||
printing facilities for Go data types are as follows:
|
|
||||||
|
|
||||||
* Pointers are dereferenced and followed
|
|
||||||
* Circular data structures are detected and handled properly
|
|
||||||
* Custom Stringer/error interfaces are optionally invoked, including
|
|
||||||
on unexported types
|
|
||||||
* Custom types which only implement the Stringer/error interfaces via
|
|
||||||
a pointer receiver are optionally invoked when passing non-pointer
|
|
||||||
variables
|
|
||||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
|
||||||
includes offsets, byte values in hex, and ASCII output (only when using
|
|
||||||
Dump style)
|
|
||||||
|
|
||||||
There are two different approaches spew allows for dumping Go data structures:
|
|
||||||
|
|
||||||
* Dump style which prints with newlines, customizable indentation,
|
|
||||||
and additional debug information such as types and all pointer addresses
|
|
||||||
used to indirect to the final value
|
|
||||||
* A custom Formatter interface that integrates cleanly with the standard fmt
|
|
||||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
|
||||||
similar to the default %v while providing the additional functionality
|
|
||||||
outlined above and passing unsupported format verbs such as %x and %q
|
|
||||||
along to fmt
|
|
||||||
|
|
||||||
Quick Start
|
|
||||||
|
|
||||||
This section demonstrates how to quickly get started with spew. See the
|
|
||||||
sections below for further details on formatting and configuration options.
|
|
||||||
|
|
||||||
To dump a variable with full newlines, indentation, type, and pointer
|
|
||||||
information use Dump, Fdump, or Sdump:
|
|
||||||
spew.Dump(myVar1, myVar2, ...)
|
|
||||||
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
|
||||||
str := spew.Sdump(myVar1, myVar2, ...)
|
|
||||||
|
|
||||||
Alternatively, if you would prefer to use format strings with a compacted inline
|
|
||||||
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
|
||||||
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
|
||||||
%#+v (adds types and pointer addresses):
|
|
||||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
|
||||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
|
||||||
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
|
||||||
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
|
||||||
|
|
||||||
Configuration Options
|
|
||||||
|
|
||||||
Configuration of spew is handled by fields in the ConfigState type. For
|
|
||||||
convenience, all of the top-level functions use a global state available
|
|
||||||
via the spew.Config global.
|
|
||||||
|
|
||||||
It is also possible to create a ConfigState instance that provides methods
|
|
||||||
equivalent to the top-level functions. This allows concurrent configuration
|
|
||||||
options. See the ConfigState documentation for more details.
|
|
||||||
|
|
||||||
The following configuration options are available:
|
|
||||||
* Indent
|
|
||||||
String to use for each indentation level for Dump functions.
|
|
||||||
It is a single space by default. A popular alternative is "\t".
|
|
||||||
|
|
||||||
* MaxDepth
|
|
||||||
Maximum number of levels to descend into nested data structures.
|
|
||||||
There is no limit by default.
|
|
||||||
|
|
||||||
* DisableMethods
|
|
||||||
Disables invocation of error and Stringer interface methods.
|
|
||||||
Method invocation is enabled by default.
|
|
||||||
|
|
||||||
* DisablePointerMethods
|
|
||||||
Disables invocation of error and Stringer interface methods on types
|
|
||||||
which only accept pointer receivers from non-pointer variables.
|
|
||||||
Pointer method invocation is enabled by default.
|
|
||||||
|
|
||||||
* DisablePointerAddresses
|
|
||||||
DisablePointerAddresses specifies whether to disable the printing of
|
|
||||||
pointer addresses. This is useful when diffing data structures in tests.
|
|
||||||
|
|
||||||
* DisableCapacities
|
|
||||||
DisableCapacities specifies whether to disable the printing of
|
|
||||||
capacities for arrays, slices, maps and channels. This is useful when
|
|
||||||
diffing data structures in tests.
|
|
||||||
|
|
||||||
* ContinueOnMethod
|
|
||||||
Enables recursion into types after invoking error and Stringer interface
|
|
||||||
methods. Recursion after method invocation is disabled by default.
|
|
||||||
|
|
||||||
* SortKeys
|
|
||||||
Specifies map keys should be sorted before being printed. Use
|
|
||||||
this to have a more deterministic, diffable output. Note that
|
|
||||||
only native types (bool, int, uint, floats, uintptr and string)
|
|
||||||
and types which implement error or Stringer interfaces are
|
|
||||||
supported with other types sorted according to the
|
|
||||||
reflect.Value.String() output which guarantees display
|
|
||||||
stability. Natural map order is used by default.
|
|
||||||
|
|
||||||
* SpewKeys
|
|
||||||
Specifies that, as a last resort attempt, map keys should be
|
|
||||||
spewed to strings and sorted by those strings. This is only
|
|
||||||
considered if SortKeys is true.
|
|
||||||
|
|
||||||
Dump Usage
|
|
||||||
|
|
||||||
Simply call spew.Dump with a list of variables you want to dump:
|
|
||||||
|
|
||||||
spew.Dump(myVar1, myVar2, ...)
|
|
||||||
|
|
||||||
You may also call spew.Fdump if you would prefer to output to an arbitrary
|
|
||||||
io.Writer. For example, to dump to standard error:
|
|
||||||
|
|
||||||
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
|
|
||||||
|
|
||||||
A third option is to call spew.Sdump to get the formatted output as a string:
|
|
||||||
|
|
||||||
str := spew.Sdump(myVar1, myVar2, ...)
|
|
||||||
|
|
||||||
Sample Dump Output
|
|
||||||
|
|
||||||
See the Dump example for details on the setup of the types and variables being
|
|
||||||
shown here.
|
|
||||||
|
|
||||||
(main.Foo) {
|
|
||||||
unexportedField: (*main.Bar)(0xf84002e210)({
|
|
||||||
flag: (main.Flag) flagTwo,
|
|
||||||
data: (uintptr) <nil>
|
|
||||||
}),
|
|
||||||
ExportedField: (map[interface {}]interface {}) (len=1) {
|
|
||||||
(string) (len=3) "one": (bool) true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
|
||||||
command as shown.
|
|
||||||
([]uint8) (len=32 cap=32) {
|
|
||||||
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
|
||||||
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
|
||||||
00000020 31 32 |12|
|
|
||||||
}
|
|
||||||
|
|
||||||
Custom Formatter
|
|
||||||
|
|
||||||
Spew provides a custom formatter that implements the fmt.Formatter interface
|
|
||||||
so that it integrates cleanly with standard fmt package printing functions. The
|
|
||||||
formatter is useful for inline printing of smaller data types similar to the
|
|
||||||
standard %v format specifier.
|
|
||||||
|
|
||||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
|
||||||
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
|
||||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
|
||||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
|
||||||
the width and precision arguments (however they will still work on the format
|
|
||||||
specifiers not handled by the custom formatter).
|
|
||||||
|
|
||||||
Custom Formatter Usage
|
|
||||||
|
|
||||||
The simplest way to make use of the spew custom formatter is to call one of the
|
|
||||||
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
|
||||||
functions have syntax you are most likely already familiar with:
|
|
||||||
|
|
||||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
|
||||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
|
||||||
spew.Println(myVar, myVar2)
|
|
||||||
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
|
||||||
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
|
||||||
|
|
||||||
See the Index for the full list convenience functions.
|
|
||||||
|
|
||||||
Sample Formatter Output
|
|
||||||
|
|
||||||
Double pointer to a uint8:
|
|
||||||
%v: <**>5
|
|
||||||
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
|
||||||
%#v: (**uint8)5
|
|
||||||
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
|
||||||
|
|
||||||
Pointer to circular struct with a uint8 field and a pointer to itself:
|
|
||||||
%v: <*>{1 <*><shown>}
|
|
||||||
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
|
||||||
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
|
||||||
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
|
||||||
|
|
||||||
See the Printf example for details on the setup of variables being shown
|
|
||||||
here.
|
|
||||||
|
|
||||||
Errors
|
|
||||||
|
|
||||||
Since it is possible for custom Stringer/error interfaces to panic, spew
|
|
||||||
detects them and handles them internally by printing the panic information
|
|
||||||
inline with the output. Since spew is intended to provide deep pretty printing
|
|
||||||
capabilities on structures, it intentionally does not return any errors.
|
|
||||||
*/
|
|
||||||
package spew
|
|
509
vendor/github.com/davecgh/go-spew/spew/dump.go
generated
vendored
509
vendor/github.com/davecgh/go-spew/spew/dump.go
generated
vendored
@ -1,509 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// uint8Type is a reflect.Type representing a uint8. It is used to
|
|
||||||
// convert cgo types to uint8 slices for hexdumping.
|
|
||||||
uint8Type = reflect.TypeOf(uint8(0))
|
|
||||||
|
|
||||||
// cCharRE is a regular expression that matches a cgo char.
|
|
||||||
// It is used to detect character arrays to hexdump them.
|
|
||||||
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
|
||||||
|
|
||||||
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
|
||||||
// char. It is used to detect unsigned character arrays to hexdump
|
|
||||||
// them.
|
|
||||||
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
|
||||||
|
|
||||||
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
|
||||||
// It is used to detect uint8_t arrays to hexdump them.
|
|
||||||
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
|
||||||
)
|
|
||||||
|
|
||||||
// dumpState contains information about the state of a dump operation.
|
|
||||||
type dumpState struct {
|
|
||||||
w io.Writer
|
|
||||||
depth int
|
|
||||||
pointers map[uintptr]int
|
|
||||||
ignoreNextType bool
|
|
||||||
ignoreNextIndent bool
|
|
||||||
cs *ConfigState
|
|
||||||
}
|
|
||||||
|
|
||||||
// indent performs indentation according to the depth level and cs.Indent
|
|
||||||
// option.
|
|
||||||
func (d *dumpState) indent() {
|
|
||||||
if d.ignoreNextIndent {
|
|
||||||
d.ignoreNextIndent = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpackValue returns values inside of non-nil interfaces when possible.
|
|
||||||
// This is useful for data types like structs, arrays, slices, and maps which
|
|
||||||
// can contain varying types packed inside an interface.
|
|
||||||
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
|
|
||||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
|
||||||
v = v.Elem()
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// dumpPtr handles formatting of pointers by indirecting them as necessary.
|
|
||||||
func (d *dumpState) dumpPtr(v reflect.Value) {
|
|
||||||
// Remove pointers at or below the current depth from map used to detect
|
|
||||||
// circular refs.
|
|
||||||
for k, depth := range d.pointers {
|
|
||||||
if depth >= d.depth {
|
|
||||||
delete(d.pointers, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep list of all dereferenced pointers to show later.
|
|
||||||
pointerChain := make([]uintptr, 0)
|
|
||||||
|
|
||||||
// Figure out how many levels of indirection there are by dereferencing
|
|
||||||
// pointers and unpacking interfaces down the chain while detecting circular
|
|
||||||
// references.
|
|
||||||
nilFound := false
|
|
||||||
cycleFound := false
|
|
||||||
indirects := 0
|
|
||||||
ve := v
|
|
||||||
for ve.Kind() == reflect.Ptr {
|
|
||||||
if ve.IsNil() {
|
|
||||||
nilFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
indirects++
|
|
||||||
addr := ve.Pointer()
|
|
||||||
pointerChain = append(pointerChain, addr)
|
|
||||||
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
|
|
||||||
cycleFound = true
|
|
||||||
indirects--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
d.pointers[addr] = d.depth
|
|
||||||
|
|
||||||
ve = ve.Elem()
|
|
||||||
if ve.Kind() == reflect.Interface {
|
|
||||||
if ve.IsNil() {
|
|
||||||
nilFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ve = ve.Elem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display type information.
|
|
||||||
d.w.Write(openParenBytes)
|
|
||||||
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
|
|
||||||
d.w.Write([]byte(ve.Type().String()))
|
|
||||||
d.w.Write(closeParenBytes)
|
|
||||||
|
|
||||||
// Display pointer information.
|
|
||||||
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
|
||||||
d.w.Write(openParenBytes)
|
|
||||||
for i, addr := range pointerChain {
|
|
||||||
if i > 0 {
|
|
||||||
d.w.Write(pointerChainBytes)
|
|
||||||
}
|
|
||||||
printHexPtr(d.w, addr)
|
|
||||||
}
|
|
||||||
d.w.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display dereferenced value.
|
|
||||||
d.w.Write(openParenBytes)
|
|
||||||
switch {
|
|
||||||
case nilFound == true:
|
|
||||||
d.w.Write(nilAngleBytes)
|
|
||||||
|
|
||||||
case cycleFound == true:
|
|
||||||
d.w.Write(circularBytes)
|
|
||||||
|
|
||||||
default:
|
|
||||||
d.ignoreNextType = true
|
|
||||||
d.dump(ve)
|
|
||||||
}
|
|
||||||
d.w.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
|
|
||||||
// reflection) arrays and slices are dumped in hexdump -C fashion.
|
|
||||||
func (d *dumpState) dumpSlice(v reflect.Value) {
|
|
||||||
// Determine whether this type should be hex dumped or not. Also,
|
|
||||||
// for types which should be hexdumped, try to use the underlying data
|
|
||||||
// first, then fall back to trying to convert them to a uint8 slice.
|
|
||||||
var buf []uint8
|
|
||||||
doConvert := false
|
|
||||||
doHexDump := false
|
|
||||||
numEntries := v.Len()
|
|
||||||
if numEntries > 0 {
|
|
||||||
vt := v.Index(0).Type()
|
|
||||||
vts := vt.String()
|
|
||||||
switch {
|
|
||||||
// C types that need to be converted.
|
|
||||||
case cCharRE.MatchString(vts):
|
|
||||||
fallthrough
|
|
||||||
case cUnsignedCharRE.MatchString(vts):
|
|
||||||
fallthrough
|
|
||||||
case cUint8tCharRE.MatchString(vts):
|
|
||||||
doConvert = true
|
|
||||||
|
|
||||||
// Try to use existing uint8 slices and fall back to converting
|
|
||||||
// and copying if that fails.
|
|
||||||
case vt.Kind() == reflect.Uint8:
|
|
||||||
// We need an addressable interface to convert the type
|
|
||||||
// to a byte slice. However, the reflect package won't
|
|
||||||
// give us an interface on certain things like
|
|
||||||
// unexported struct fields in order to enforce
|
|
||||||
// visibility rules. We use unsafe, when available, to
|
|
||||||
// bypass these restrictions since this package does not
|
|
||||||
// mutate the values.
|
|
||||||
vs := v
|
|
||||||
if !vs.CanInterface() || !vs.CanAddr() {
|
|
||||||
vs = unsafeReflectValue(vs)
|
|
||||||
}
|
|
||||||
if !UnsafeDisabled {
|
|
||||||
vs = vs.Slice(0, numEntries)
|
|
||||||
|
|
||||||
// Use the existing uint8 slice if it can be
|
|
||||||
// type asserted.
|
|
||||||
iface := vs.Interface()
|
|
||||||
if slice, ok := iface.([]uint8); ok {
|
|
||||||
buf = slice
|
|
||||||
doHexDump = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The underlying data needs to be converted if it can't
|
|
||||||
// be type asserted to a uint8 slice.
|
|
||||||
doConvert = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy and convert the underlying type if needed.
|
|
||||||
if doConvert && vt.ConvertibleTo(uint8Type) {
|
|
||||||
// Convert and copy each element into a uint8 byte
|
|
||||||
// slice.
|
|
||||||
buf = make([]uint8, numEntries)
|
|
||||||
for i := 0; i < numEntries; i++ {
|
|
||||||
vv := v.Index(i)
|
|
||||||
buf[i] = uint8(vv.Convert(uint8Type).Uint())
|
|
||||||
}
|
|
||||||
doHexDump = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hexdump the entire slice as needed.
|
|
||||||
if doHexDump {
|
|
||||||
indent := strings.Repeat(d.cs.Indent, d.depth)
|
|
||||||
str := indent + hex.Dump(buf)
|
|
||||||
str = strings.Replace(str, "\n", "\n"+indent, -1)
|
|
||||||
str = strings.TrimRight(str, d.cs.Indent)
|
|
||||||
d.w.Write([]byte(str))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively call dump for each item.
|
|
||||||
for i := 0; i < numEntries; i++ {
|
|
||||||
d.dump(d.unpackValue(v.Index(i)))
|
|
||||||
if i < (numEntries - 1) {
|
|
||||||
d.w.Write(commaNewlineBytes)
|
|
||||||
} else {
|
|
||||||
d.w.Write(newlineBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dump is the main workhorse for dumping a value. It uses the passed reflect
|
|
||||||
// value to figure out what kind of object we are dealing with and formats it
|
|
||||||
// appropriately. It is a recursive function, however circular data structures
|
|
||||||
// are detected and handled properly.
|
|
||||||
func (d *dumpState) dump(v reflect.Value) {
|
|
||||||
// Handle invalid reflect values immediately.
|
|
||||||
kind := v.Kind()
|
|
||||||
if kind == reflect.Invalid {
|
|
||||||
d.w.Write(invalidAngleBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pointers specially.
|
|
||||||
if kind == reflect.Ptr {
|
|
||||||
d.indent()
|
|
||||||
d.dumpPtr(v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print type information unless already handled elsewhere.
|
|
||||||
if !d.ignoreNextType {
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(openParenBytes)
|
|
||||||
d.w.Write([]byte(v.Type().String()))
|
|
||||||
d.w.Write(closeParenBytes)
|
|
||||||
d.w.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
d.ignoreNextType = false
|
|
||||||
|
|
||||||
// Display length and capacity if the built-in len and cap functions
|
|
||||||
// work with the value's kind and the len/cap itself is non-zero.
|
|
||||||
valueLen, valueCap := 0, 0
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Array, reflect.Slice, reflect.Chan:
|
|
||||||
valueLen, valueCap = v.Len(), v.Cap()
|
|
||||||
case reflect.Map, reflect.String:
|
|
||||||
valueLen = v.Len()
|
|
||||||
}
|
|
||||||
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
|
||||||
d.w.Write(openParenBytes)
|
|
||||||
if valueLen != 0 {
|
|
||||||
d.w.Write(lenEqualsBytes)
|
|
||||||
printInt(d.w, int64(valueLen), 10)
|
|
||||||
}
|
|
||||||
if !d.cs.DisableCapacities && valueCap != 0 {
|
|
||||||
if valueLen != 0 {
|
|
||||||
d.w.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
d.w.Write(capEqualsBytes)
|
|
||||||
printInt(d.w, int64(valueCap), 10)
|
|
||||||
}
|
|
||||||
d.w.Write(closeParenBytes)
|
|
||||||
d.w.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Stringer/error interfaces if they exist and the handle methods flag
|
|
||||||
// is enabled
|
|
||||||
if !d.cs.DisableMethods {
|
|
||||||
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
|
||||||
if handled := handleMethods(d.cs, d.w, v); handled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case reflect.Invalid:
|
|
||||||
// Do nothing. We should never get here since invalid has already
|
|
||||||
// been handled above.
|
|
||||||
|
|
||||||
case reflect.Bool:
|
|
||||||
printBool(d.w, v.Bool())
|
|
||||||
|
|
||||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
||||||
printInt(d.w, v.Int(), 10)
|
|
||||||
|
|
||||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
||||||
printUint(d.w, v.Uint(), 10)
|
|
||||||
|
|
||||||
case reflect.Float32:
|
|
||||||
printFloat(d.w, v.Float(), 32)
|
|
||||||
|
|
||||||
case reflect.Float64:
|
|
||||||
printFloat(d.w, v.Float(), 64)
|
|
||||||
|
|
||||||
case reflect.Complex64:
|
|
||||||
printComplex(d.w, v.Complex(), 32)
|
|
||||||
|
|
||||||
case reflect.Complex128:
|
|
||||||
printComplex(d.w, v.Complex(), 64)
|
|
||||||
|
|
||||||
case reflect.Slice:
|
|
||||||
if v.IsNil() {
|
|
||||||
d.w.Write(nilAngleBytes)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
|
|
||||||
case reflect.Array:
|
|
||||||
d.w.Write(openBraceNewlineBytes)
|
|
||||||
d.depth++
|
|
||||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(maxNewlineBytes)
|
|
||||||
} else {
|
|
||||||
d.dumpSlice(v)
|
|
||||||
}
|
|
||||||
d.depth--
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(closeBraceBytes)
|
|
||||||
|
|
||||||
case reflect.String:
|
|
||||||
d.w.Write([]byte(strconv.Quote(v.String())))
|
|
||||||
|
|
||||||
case reflect.Interface:
|
|
||||||
// The only time we should get here is for nil interfaces due to
|
|
||||||
// unpackValue calls.
|
|
||||||
if v.IsNil() {
|
|
||||||
d.w.Write(nilAngleBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.Ptr:
|
|
||||||
// Do nothing. We should never get here since pointers have already
|
|
||||||
// been handled above.
|
|
||||||
|
|
||||||
case reflect.Map:
|
|
||||||
// nil maps should be indicated as different than empty maps
|
|
||||||
if v.IsNil() {
|
|
||||||
d.w.Write(nilAngleBytes)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
d.w.Write(openBraceNewlineBytes)
|
|
||||||
d.depth++
|
|
||||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(maxNewlineBytes)
|
|
||||||
} else {
|
|
||||||
numEntries := v.Len()
|
|
||||||
keys := v.MapKeys()
|
|
||||||
if d.cs.SortKeys {
|
|
||||||
sortValues(keys, d.cs)
|
|
||||||
}
|
|
||||||
for i, key := range keys {
|
|
||||||
d.dump(d.unpackValue(key))
|
|
||||||
d.w.Write(colonSpaceBytes)
|
|
||||||
d.ignoreNextIndent = true
|
|
||||||
d.dump(d.unpackValue(v.MapIndex(key)))
|
|
||||||
if i < (numEntries - 1) {
|
|
||||||
d.w.Write(commaNewlineBytes)
|
|
||||||
} else {
|
|
||||||
d.w.Write(newlineBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.depth--
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(closeBraceBytes)
|
|
||||||
|
|
||||||
case reflect.Struct:
|
|
||||||
d.w.Write(openBraceNewlineBytes)
|
|
||||||
d.depth++
|
|
||||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(maxNewlineBytes)
|
|
||||||
} else {
|
|
||||||
vt := v.Type()
|
|
||||||
numFields := v.NumField()
|
|
||||||
for i := 0; i < numFields; i++ {
|
|
||||||
d.indent()
|
|
||||||
vtf := vt.Field(i)
|
|
||||||
d.w.Write([]byte(vtf.Name))
|
|
||||||
d.w.Write(colonSpaceBytes)
|
|
||||||
d.ignoreNextIndent = true
|
|
||||||
d.dump(d.unpackValue(v.Field(i)))
|
|
||||||
if i < (numFields - 1) {
|
|
||||||
d.w.Write(commaNewlineBytes)
|
|
||||||
} else {
|
|
||||||
d.w.Write(newlineBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.depth--
|
|
||||||
d.indent()
|
|
||||||
d.w.Write(closeBraceBytes)
|
|
||||||
|
|
||||||
case reflect.Uintptr:
|
|
||||||
printHexPtr(d.w, uintptr(v.Uint()))
|
|
||||||
|
|
||||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
|
||||||
printHexPtr(d.w, v.Pointer())
|
|
||||||
|
|
||||||
// There were not any other types at the time this code was written, but
|
|
||||||
// fall back to letting the default fmt package handle it in case any new
|
|
||||||
// types are added.
|
|
||||||
default:
|
|
||||||
if v.CanInterface() {
|
|
||||||
fmt.Fprintf(d.w, "%v", v.Interface())
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(d.w, "%v", v.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fdump is a helper function to consolidate the logic from the various public
|
|
||||||
// methods which take varying writers and config states.
|
|
||||||
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
|
|
||||||
for _, arg := range a {
|
|
||||||
if arg == nil {
|
|
||||||
w.Write(interfaceBytes)
|
|
||||||
w.Write(spaceBytes)
|
|
||||||
w.Write(nilAngleBytes)
|
|
||||||
w.Write(newlineBytes)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
d := dumpState{w: w, cs: cs}
|
|
||||||
d.pointers = make(map[uintptr]int)
|
|
||||||
d.dump(reflect.ValueOf(arg))
|
|
||||||
d.w.Write(newlineBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
|
||||||
// exactly the same as Dump.
|
|
||||||
func Fdump(w io.Writer, a ...interface{}) {
|
|
||||||
fdump(&Config, w, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sdump returns a string with the passed arguments formatted exactly the same
|
|
||||||
// as Dump.
|
|
||||||
func Sdump(a ...interface{}) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
fdump(&Config, &buf, a...)
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Dump displays the passed parameters to standard out with newlines, customizable
|
|
||||||
indentation, and additional debug information such as complete types and all
|
|
||||||
pointer addresses used to indirect to the final value. It provides the
|
|
||||||
following features over the built-in printing facilities provided by the fmt
|
|
||||||
package:
|
|
||||||
|
|
||||||
* Pointers are dereferenced and followed
|
|
||||||
* Circular data structures are detected and handled properly
|
|
||||||
* Custom Stringer/error interfaces are optionally invoked, including
|
|
||||||
on unexported types
|
|
||||||
* Custom types which only implement the Stringer/error interfaces via
|
|
||||||
a pointer receiver are optionally invoked when passing non-pointer
|
|
||||||
variables
|
|
||||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
|
||||||
includes offsets, byte values in hex, and ASCII output
|
|
||||||
|
|
||||||
The configuration options are controlled by an exported package global,
|
|
||||||
spew.Config. See ConfigState for options documentation.
|
|
||||||
|
|
||||||
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
|
||||||
get the formatted result as a string.
|
|
||||||
*/
|
|
||||||
func Dump(a ...interface{}) {
|
|
||||||
fdump(&Config, os.Stdout, a...)
|
|
||||||
}
|
|
419
vendor/github.com/davecgh/go-spew/spew/format.go
generated
vendored
419
vendor/github.com/davecgh/go-spew/spew/format.go
generated
vendored
@ -1,419 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// supportedFlags is a list of all the character flags supported by fmt package.
|
|
||||||
const supportedFlags = "0-+# "
|
|
||||||
|
|
||||||
// formatState implements the fmt.Formatter interface and contains information
|
|
||||||
// about the state of a formatting operation. The NewFormatter function can
|
|
||||||
// be used to get a new Formatter which can be used directly as arguments
|
|
||||||
// in standard fmt package printing calls.
|
|
||||||
type formatState struct {
|
|
||||||
value interface{}
|
|
||||||
fs fmt.State
|
|
||||||
depth int
|
|
||||||
pointers map[uintptr]int
|
|
||||||
ignoreNextType bool
|
|
||||||
cs *ConfigState
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildDefaultFormat recreates the original format string without precision
|
|
||||||
// and width information to pass in to fmt.Sprintf in the case of an
|
|
||||||
// unrecognized type. Unless new types are added to the language, this
|
|
||||||
// function won't ever be called.
|
|
||||||
func (f *formatState) buildDefaultFormat() (format string) {
|
|
||||||
buf := bytes.NewBuffer(percentBytes)
|
|
||||||
|
|
||||||
for _, flag := range supportedFlags {
|
|
||||||
if f.fs.Flag(int(flag)) {
|
|
||||||
buf.WriteRune(flag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteRune('v')
|
|
||||||
|
|
||||||
format = buf.String()
|
|
||||||
return format
|
|
||||||
}
|
|
||||||
|
|
||||||
// constructOrigFormat recreates the original format string including precision
|
|
||||||
// and width information to pass along to the standard fmt package. This allows
|
|
||||||
// automatic deferral of all format strings this package doesn't support.
|
|
||||||
func (f *formatState) constructOrigFormat(verb rune) (format string) {
|
|
||||||
buf := bytes.NewBuffer(percentBytes)
|
|
||||||
|
|
||||||
for _, flag := range supportedFlags {
|
|
||||||
if f.fs.Flag(int(flag)) {
|
|
||||||
buf.WriteRune(flag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if width, ok := f.fs.Width(); ok {
|
|
||||||
buf.WriteString(strconv.Itoa(width))
|
|
||||||
}
|
|
||||||
|
|
||||||
if precision, ok := f.fs.Precision(); ok {
|
|
||||||
buf.Write(precisionBytes)
|
|
||||||
buf.WriteString(strconv.Itoa(precision))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteRune(verb)
|
|
||||||
|
|
||||||
format = buf.String()
|
|
||||||
return format
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpackValue returns values inside of non-nil interfaces when possible and
|
|
||||||
// ensures that types for values which have been unpacked from an interface
|
|
||||||
// are displayed when the show types flag is also set.
|
|
||||||
// This is useful for data types like structs, arrays, slices, and maps which
|
|
||||||
// can contain varying types packed inside an interface.
|
|
||||||
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
|
|
||||||
if v.Kind() == reflect.Interface {
|
|
||||||
f.ignoreNextType = false
|
|
||||||
if !v.IsNil() {
|
|
||||||
v = v.Elem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatPtr handles formatting of pointers by indirecting them as necessary.
|
|
||||||
func (f *formatState) formatPtr(v reflect.Value) {
|
|
||||||
// Display nil if top level pointer is nil.
|
|
||||||
showTypes := f.fs.Flag('#')
|
|
||||||
if v.IsNil() && (!showTypes || f.ignoreNextType) {
|
|
||||||
f.fs.Write(nilAngleBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove pointers at or below the current depth from map used to detect
|
|
||||||
// circular refs.
|
|
||||||
for k, depth := range f.pointers {
|
|
||||||
if depth >= f.depth {
|
|
||||||
delete(f.pointers, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep list of all dereferenced pointers to possibly show later.
|
|
||||||
pointerChain := make([]uintptr, 0)
|
|
||||||
|
|
||||||
// Figure out how many levels of indirection there are by derferencing
|
|
||||||
// pointers and unpacking interfaces down the chain while detecting circular
|
|
||||||
// references.
|
|
||||||
nilFound := false
|
|
||||||
cycleFound := false
|
|
||||||
indirects := 0
|
|
||||||
ve := v
|
|
||||||
for ve.Kind() == reflect.Ptr {
|
|
||||||
if ve.IsNil() {
|
|
||||||
nilFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
indirects++
|
|
||||||
addr := ve.Pointer()
|
|
||||||
pointerChain = append(pointerChain, addr)
|
|
||||||
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
|
|
||||||
cycleFound = true
|
|
||||||
indirects--
|
|
||||||
break
|
|
||||||
}
|
|
||||||
f.pointers[addr] = f.depth
|
|
||||||
|
|
||||||
ve = ve.Elem()
|
|
||||||
if ve.Kind() == reflect.Interface {
|
|
||||||
if ve.IsNil() {
|
|
||||||
nilFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ve = ve.Elem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display type or indirection level depending on flags.
|
|
||||||
if showTypes && !f.ignoreNextType {
|
|
||||||
f.fs.Write(openParenBytes)
|
|
||||||
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
|
|
||||||
f.fs.Write([]byte(ve.Type().String()))
|
|
||||||
f.fs.Write(closeParenBytes)
|
|
||||||
} else {
|
|
||||||
if nilFound || cycleFound {
|
|
||||||
indirects += strings.Count(ve.Type().String(), "*")
|
|
||||||
}
|
|
||||||
f.fs.Write(openAngleBytes)
|
|
||||||
f.fs.Write([]byte(strings.Repeat("*", indirects)))
|
|
||||||
f.fs.Write(closeAngleBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display pointer information depending on flags.
|
|
||||||
if f.fs.Flag('+') && (len(pointerChain) > 0) {
|
|
||||||
f.fs.Write(openParenBytes)
|
|
||||||
for i, addr := range pointerChain {
|
|
||||||
if i > 0 {
|
|
||||||
f.fs.Write(pointerChainBytes)
|
|
||||||
}
|
|
||||||
printHexPtr(f.fs, addr)
|
|
||||||
}
|
|
||||||
f.fs.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display dereferenced value.
|
|
||||||
switch {
|
|
||||||
case nilFound == true:
|
|
||||||
f.fs.Write(nilAngleBytes)
|
|
||||||
|
|
||||||
case cycleFound == true:
|
|
||||||
f.fs.Write(circularShortBytes)
|
|
||||||
|
|
||||||
default:
|
|
||||||
f.ignoreNextType = true
|
|
||||||
f.format(ve)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// format is the main workhorse for providing the Formatter interface. It
|
|
||||||
// uses the passed reflect value to figure out what kind of object we are
|
|
||||||
// dealing with and formats it appropriately. It is a recursive function,
|
|
||||||
// however circular data structures are detected and handled properly.
|
|
||||||
func (f *formatState) format(v reflect.Value) {
|
|
||||||
// Handle invalid reflect values immediately.
|
|
||||||
kind := v.Kind()
|
|
||||||
if kind == reflect.Invalid {
|
|
||||||
f.fs.Write(invalidAngleBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pointers specially.
|
|
||||||
if kind == reflect.Ptr {
|
|
||||||
f.formatPtr(v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print type information unless already handled elsewhere.
|
|
||||||
if !f.ignoreNextType && f.fs.Flag('#') {
|
|
||||||
f.fs.Write(openParenBytes)
|
|
||||||
f.fs.Write([]byte(v.Type().String()))
|
|
||||||
f.fs.Write(closeParenBytes)
|
|
||||||
}
|
|
||||||
f.ignoreNextType = false
|
|
||||||
|
|
||||||
// Call Stringer/error interfaces if they exist and the handle methods
|
|
||||||
// flag is enabled.
|
|
||||||
if !f.cs.DisableMethods {
|
|
||||||
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
|
||||||
if handled := handleMethods(f.cs, f.fs, v); handled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case reflect.Invalid:
|
|
||||||
// Do nothing. We should never get here since invalid has already
|
|
||||||
// been handled above.
|
|
||||||
|
|
||||||
case reflect.Bool:
|
|
||||||
printBool(f.fs, v.Bool())
|
|
||||||
|
|
||||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
||||||
printInt(f.fs, v.Int(), 10)
|
|
||||||
|
|
||||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
||||||
printUint(f.fs, v.Uint(), 10)
|
|
||||||
|
|
||||||
case reflect.Float32:
|
|
||||||
printFloat(f.fs, v.Float(), 32)
|
|
||||||
|
|
||||||
case reflect.Float64:
|
|
||||||
printFloat(f.fs, v.Float(), 64)
|
|
||||||
|
|
||||||
case reflect.Complex64:
|
|
||||||
printComplex(f.fs, v.Complex(), 32)
|
|
||||||
|
|
||||||
case reflect.Complex128:
|
|
||||||
printComplex(f.fs, v.Complex(), 64)
|
|
||||||
|
|
||||||
case reflect.Slice:
|
|
||||||
if v.IsNil() {
|
|
||||||
f.fs.Write(nilAngleBytes)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
|
|
||||||
case reflect.Array:
|
|
||||||
f.fs.Write(openBracketBytes)
|
|
||||||
f.depth++
|
|
||||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
|
||||||
f.fs.Write(maxShortBytes)
|
|
||||||
} else {
|
|
||||||
numEntries := v.Len()
|
|
||||||
for i := 0; i < numEntries; i++ {
|
|
||||||
if i > 0 {
|
|
||||||
f.fs.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
f.ignoreNextType = true
|
|
||||||
f.format(f.unpackValue(v.Index(i)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.depth--
|
|
||||||
f.fs.Write(closeBracketBytes)
|
|
||||||
|
|
||||||
case reflect.String:
|
|
||||||
f.fs.Write([]byte(v.String()))
|
|
||||||
|
|
||||||
case reflect.Interface:
|
|
||||||
// The only time we should get here is for nil interfaces due to
|
|
||||||
// unpackValue calls.
|
|
||||||
if v.IsNil() {
|
|
||||||
f.fs.Write(nilAngleBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.Ptr:
|
|
||||||
// Do nothing. We should never get here since pointers have already
|
|
||||||
// been handled above.
|
|
||||||
|
|
||||||
case reflect.Map:
|
|
||||||
// nil maps should be indicated as different than empty maps
|
|
||||||
if v.IsNil() {
|
|
||||||
f.fs.Write(nilAngleBytes)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
f.fs.Write(openMapBytes)
|
|
||||||
f.depth++
|
|
||||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
|
||||||
f.fs.Write(maxShortBytes)
|
|
||||||
} else {
|
|
||||||
keys := v.MapKeys()
|
|
||||||
if f.cs.SortKeys {
|
|
||||||
sortValues(keys, f.cs)
|
|
||||||
}
|
|
||||||
for i, key := range keys {
|
|
||||||
if i > 0 {
|
|
||||||
f.fs.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
f.ignoreNextType = true
|
|
||||||
f.format(f.unpackValue(key))
|
|
||||||
f.fs.Write(colonBytes)
|
|
||||||
f.ignoreNextType = true
|
|
||||||
f.format(f.unpackValue(v.MapIndex(key)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.depth--
|
|
||||||
f.fs.Write(closeMapBytes)
|
|
||||||
|
|
||||||
case reflect.Struct:
|
|
||||||
numFields := v.NumField()
|
|
||||||
f.fs.Write(openBraceBytes)
|
|
||||||
f.depth++
|
|
||||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
|
||||||
f.fs.Write(maxShortBytes)
|
|
||||||
} else {
|
|
||||||
vt := v.Type()
|
|
||||||
for i := 0; i < numFields; i++ {
|
|
||||||
if i > 0 {
|
|
||||||
f.fs.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
vtf := vt.Field(i)
|
|
||||||
if f.fs.Flag('+') || f.fs.Flag('#') {
|
|
||||||
f.fs.Write([]byte(vtf.Name))
|
|
||||||
f.fs.Write(colonBytes)
|
|
||||||
}
|
|
||||||
f.format(f.unpackValue(v.Field(i)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.depth--
|
|
||||||
f.fs.Write(closeBraceBytes)
|
|
||||||
|
|
||||||
case reflect.Uintptr:
|
|
||||||
printHexPtr(f.fs, uintptr(v.Uint()))
|
|
||||||
|
|
||||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
|
||||||
printHexPtr(f.fs, v.Pointer())
|
|
||||||
|
|
||||||
// There were not any other types at the time this code was written, but
|
|
||||||
// fall back to letting the default fmt package handle it if any get added.
|
|
||||||
default:
|
|
||||||
format := f.buildDefaultFormat()
|
|
||||||
if v.CanInterface() {
|
|
||||||
fmt.Fprintf(f.fs, format, v.Interface())
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(f.fs, format, v.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
|
|
||||||
// details.
|
|
||||||
func (f *formatState) Format(fs fmt.State, verb rune) {
|
|
||||||
f.fs = fs
|
|
||||||
|
|
||||||
// Use standard formatting for verbs that are not v.
|
|
||||||
if verb != 'v' {
|
|
||||||
format := f.constructOrigFormat(verb)
|
|
||||||
fmt.Fprintf(fs, format, f.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.value == nil {
|
|
||||||
if fs.Flag('#') {
|
|
||||||
fs.Write(interfaceBytes)
|
|
||||||
}
|
|
||||||
fs.Write(nilAngleBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.format(reflect.ValueOf(f.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFormatter is a helper function to consolidate the logic from the various
|
|
||||||
// public methods which take varying config states.
|
|
||||||
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
|
|
||||||
fs := &formatState{value: v, cs: cs}
|
|
||||||
fs.pointers = make(map[uintptr]int)
|
|
||||||
return fs
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
|
||||||
interface. As a result, it integrates cleanly with standard fmt package
|
|
||||||
printing functions. The formatter is useful for inline printing of smaller data
|
|
||||||
types similar to the standard %v format specifier.
|
|
||||||
|
|
||||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
|
||||||
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
|
||||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
|
||||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
|
||||||
the width and precision arguments (however they will still work on the format
|
|
||||||
specifiers not handled by the custom formatter).
|
|
||||||
|
|
||||||
Typically this function shouldn't be called directly. It is much easier to make
|
|
||||||
use of the custom formatter by calling one of the convenience functions such as
|
|
||||||
Printf, Println, or Fprintf.
|
|
||||||
*/
|
|
||||||
func NewFormatter(v interface{}) fmt.Formatter {
|
|
||||||
return newFormatter(&Config, v)
|
|
||||||
}
|
|
148
vendor/github.com/davecgh/go-spew/spew/spew.go
generated
vendored
148
vendor/github.com/davecgh/go-spew/spew/spew.go
generated
vendored
@ -1,148 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
|
||||||
*
|
|
||||||
* Permission to use, copy, modify, and distribute this software for any
|
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
|
||||||
* copyright notice and this permission notice appear in all copies.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package spew
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the formatted string as a value that satisfies error. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Errorf(format string, a ...interface{}) (err error) {
|
|
||||||
return fmt.Errorf(format, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprint(w, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprintf(w, format, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Fprintln(w, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Print(a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Print(convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Printf(format string, a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Printf(format, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the number of bytes written and any write error encountered. See
|
|
||||||
// NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Println(a ...interface{}) (n int, err error) {
|
|
||||||
return fmt.Println(convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Sprint(a ...interface{}) string {
|
|
||||||
return fmt.Sprint(convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
|
||||||
// passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Sprintf(format string, a ...interface{}) string {
|
|
||||||
return fmt.Sprintf(format, convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
|
||||||
// were passed with a default Formatter interface returned by NewFormatter. It
|
|
||||||
// returns the resulting string. See NewFormatter for formatting details.
|
|
||||||
//
|
|
||||||
// This function is shorthand for the following syntax:
|
|
||||||
//
|
|
||||||
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
|
|
||||||
func Sprintln(a ...interface{}) string {
|
|
||||||
return fmt.Sprintln(convertArgs(a)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertArgs accepts a slice of arguments and returns a slice of the same
|
|
||||||
// length with each argument converted to a default spew Formatter interface.
|
|
||||||
func convertArgs(args []interface{}) (formatters []interface{}) {
|
|
||||||
formatters = make([]interface{}, len(args))
|
|
||||||
for index, arg := range args {
|
|
||||||
formatters[index] = NewFormatter(arg)
|
|
||||||
}
|
|
||||||
return formatters
|
|
||||||
}
|
|
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()
|
||||||
|
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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user