From dcd461d29f21a9626d5298a03283b6d8b46312c3 Mon Sep 17 00:00:00 2001 From: Andrei Miulescu Date: Sun, 12 Aug 2018 19:31:27 +1000 Subject: [PATCH 01/19] Restrucure project in a way where it is more modular --- main.go | 31 ++++---- pkg/app/app.go | 49 ++++++++++++ pkg/commands/git.go | 71 +++++++++++++++++ gitcommands.go => pkg/commands/gitcommands.go | 27 ------- pkg/commands/os.go | 77 +++++++++++++++++++ pkg/config/app_config.go | 45 +++++++++++ branch.go => pkg/git/branch.go | 12 +-- .../git/branch_list_builder.go | 2 +- pkg/git/git_structs.go | 28 +++++++ gui.go => pkg/gui/gui.go | 11 +-- keybindings.go => pkg/gui/keybindings.go | 2 +- .../gui/panels/branches_panel.go | 0 .../gui/panels/commit_message_panel.go | 0 .../gui/panels/commits_panel.go | 0 .../gui/panels/confirmation_panel.go | 0 .../gui/panels/files_panel.go | 0 .../gui/panels/merge_panel.go | 0 .../gui/panels/stash_panel.go | 0 .../gui/panels/status_panel.go | 0 view_helpers.go => pkg/gui/view_helpers.go | 6 +- pkg/utils/utils.go | 49 ++++++++++++ utils.go | 5 -- 22 files changed, 357 insertions(+), 58 deletions(-) create mode 100644 pkg/app/app.go create mode 100644 pkg/commands/git.go rename gitcommands.go => pkg/commands/gitcommands.go (95%) create mode 100644 pkg/commands/os.go create mode 100644 pkg/config/app_config.go rename branch.go => pkg/git/branch.go (61%) rename branch_list_builder.go => pkg/git/branch_list_builder.go (99%) create mode 100644 pkg/git/git_structs.go rename gui.go => pkg/gui/gui.go (97%) rename keybindings.go => pkg/gui/keybindings.go (99%) rename branches_panel.go => pkg/gui/panels/branches_panel.go (100%) rename commit_message_panel.go => pkg/gui/panels/commit_message_panel.go (100%) rename commits_panel.go => pkg/gui/panels/commits_panel.go (100%) rename confirmation_panel.go => pkg/gui/panels/confirmation_panel.go (100%) rename files_panel.go => pkg/gui/panels/files_panel.go (100%) rename merge_panel.go => pkg/gui/panels/merge_panel.go (100%) rename stash_panel.go => pkg/gui/panels/stash_panel.go (100%) rename status_panel.go => pkg/gui/panels/status_panel.go (100%) rename view_helpers.go => pkg/gui/view_helpers.go (98%) create mode 100644 pkg/utils/utils.go diff --git a/main.go b/main.go index aec4051bf..aa157c108 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,8 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/app" + "github.com/jesseduffield/lazygit/pkg/config" git "gopkg.in/src-d/go-git.v4" ) @@ -24,8 +26,8 @@ var ( commit string version = "unversioned" + date string - date string debuggingFlag = flag.Bool("debug", false, "a boolean") versionFlag = flag.Bool("v", false, "Print the current version") @@ -77,15 +79,6 @@ func localLog(path string, objects ...interface{}) { } } -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 // 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 @@ -112,7 +105,6 @@ func setupWorktree() { } func main() { - devLog("\n\n\n\n\n\n\n\n\n\n") flag.Parse() if version == "unversioned" { version = fallbackVersion() @@ -121,9 +113,22 @@ func main() { fmt.Printf("commit=%s, build date=%s, version=%s", commit, date, version) os.Exit(0) } - verifyInGitRepo() - navigateToRepoRootDirectory() + appConfig := &config.AppConfig{ + Name: "lazygit", + Version: version, + Commit: commit, + BuildDate: date, + Debug: *debuggingFlag, + } + app, err := app.NewApp(appConfig) + app.Log.Info(err) + + app.GitCommand.SetupGit() + // TODO remove this once r, w not used setupWorktree() + + app.Gui.Run() + for { if err := run(); err != nil { if err == gocui.ErrQuit { diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 000000000..726567b01 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,49 @@ +package app + +import ( + "io" + + "github.com/Sirupsen/logrus" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/config" +) + +// App struct +type App struct { + closers []io.Closer + + Config config.AppConfigurer + Log *logrus.Logger + OSCommand *commands.OSCommand + GitCommand *commands.GitCommand +} + +// NewApp retruns a new applications +func NewApp(config config.AppConfigurer) (*App, error) { + app := &App{ + closers: []io.Closer{}, + Config: config, + } + var err error + app.Log = logrus.New() + 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 + } + 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 +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go new file mode 100644 index 000000000..17855b98e --- /dev/null +++ b/pkg/commands/git.go @@ -0,0 +1,71 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/Sirupsen/logrus" + git "gopkg.in/src-d/go-git.v4" +) + +// GitCommand is our main git interface +type GitCommand struct { + Log *logrus.Logger + OSCommand *OSCommand + Worktree *git.Worktree + Repo *git.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() +} + +func (c *GitCommand) GitIgnore(filename string) { + if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { + panic(err) + } +} + +func (c *GitCommand) verifyInGitRepo() { + if output, err := c.OSCommand.RunCommand("git status"); err != nil { + fmt.Println(output) + os.Exit(1) + } +} + +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() { + var err error + r, err := git.PlainOpen(".") + if err != nil { + panic(err) + } + c.Repo = r + + w, err := r.Worktree() + c.Worktree = w + if err != nil { + panic(err) + } + c.Worktree = w +} diff --git a/gitcommands.go b/pkg/commands/gitcommands.go similarity index 95% rename from gitcommands.go rename to pkg/commands/gitcommands.go index 14dee6dac..bdc23fef2 100644 --- a/gitcommands.go +++ b/pkg/commands/gitcommands.go @@ -21,33 +21,6 @@ var ( 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)) diff --git a/pkg/commands/os.go b/pkg/commands/os.go new file mode 100644 index 000000000..c36990aff --- /dev/null +++ b/pkg/commands/os.go @@ -0,0 +1,77 @@ +package commands + +import ( + "os/exec" + "runtime" + "strings" + + "github.com/Sirupsen/logrus" +) + +// 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: "\"", + } + } +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go new file mode 100644 index 000000000..98e56dea2 --- /dev/null +++ b/pkg/config/app_config.go @@ -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 +} diff --git a/branch.go b/pkg/git/branch.go similarity index 61% rename from branch.go rename to pkg/git/branch.go index 78c2e55aa..e0fd75a73 100644 --- a/branch.go +++ b/pkg/git/branch.go @@ -1,4 +1,4 @@ -package main +package git import ( "strings" @@ -12,11 +12,13 @@ type Branch struct { Recency string } -func (b *Branch) getDisplayString() string { - return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor()) -} +// GetDisplayString returns the dispaly string of branch +// func (b *Branch) GetDisplayString() string { +// return gui.withPadding(b.Recency, 4) + gui.coloredString(b.Name, b.getColor()) +// } -func (b *Branch) getColor() color.Attribute { +// GetColor branch color +func (b *Branch) GetColor() color.Attribute { switch b.getType() { case "feature": return color.FgGreen diff --git a/branch_list_builder.go b/pkg/git/branch_list_builder.go similarity index 99% rename from branch_list_builder.go rename to pkg/git/branch_list_builder.go index 1d4dc338d..1abf11fcc 100644 --- a/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -1,4 +1,4 @@ -package main +package git import ( "regexp" diff --git a/pkg/git/git_structs.go b/pkg/git/git_structs.go new file mode 100644 index 000000000..711f25f4b --- /dev/null +++ b/pkg/git/git_structs.go @@ -0,0 +1,28 @@ +package git + +// 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 +} diff --git a/gui.go b/pkg/gui/gui.go similarity index 97% rename from gui.go rename to pkg/gui/gui.go index 589dabea3..f35a4811f 100644 --- a/gui.go +++ b/pkg/gui/gui.go @@ -1,4 +1,4 @@ -package main +package gui import ( @@ -13,16 +13,17 @@ import ( "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/git" ) // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false type stateType struct { - GitFiles []GitFile - Branches []Branch - Commits []Commit - StashEntries []StashEntry + GitFiles []git.File + Branches []git.Branch + Commits []git.Commit + StashEntries []git.StashEntry PreviousView string HasMergeConflicts bool ConflictIndex int diff --git a/keybindings.go b/pkg/gui/keybindings.go similarity index 99% rename from keybindings.go rename to pkg/gui/keybindings.go index afaa09527..23f320ceb 100644 --- a/keybindings.go +++ b/pkg/gui/keybindings.go @@ -1,4 +1,4 @@ -package main +package gui import "github.com/jesseduffield/gocui" diff --git a/branches_panel.go b/pkg/gui/panels/branches_panel.go similarity index 100% rename from branches_panel.go rename to pkg/gui/panels/branches_panel.go diff --git a/commit_message_panel.go b/pkg/gui/panels/commit_message_panel.go similarity index 100% rename from commit_message_panel.go rename to pkg/gui/panels/commit_message_panel.go diff --git a/commits_panel.go b/pkg/gui/panels/commits_panel.go similarity index 100% rename from commits_panel.go rename to pkg/gui/panels/commits_panel.go diff --git a/confirmation_panel.go b/pkg/gui/panels/confirmation_panel.go similarity index 100% rename from confirmation_panel.go rename to pkg/gui/panels/confirmation_panel.go diff --git a/files_panel.go b/pkg/gui/panels/files_panel.go similarity index 100% rename from files_panel.go rename to pkg/gui/panels/files_panel.go diff --git a/merge_panel.go b/pkg/gui/panels/merge_panel.go similarity index 100% rename from merge_panel.go rename to pkg/gui/panels/merge_panel.go diff --git a/stash_panel.go b/pkg/gui/panels/stash_panel.go similarity index 100% rename from stash_panel.go rename to pkg/gui/panels/stash_panel.go diff --git a/status_panel.go b/pkg/gui/panels/status_panel.go similarity index 100% rename from status_panel.go rename to pkg/gui/panels/status_panel.go diff --git a/view_helpers.go b/pkg/gui/view_helpers.go similarity index 98% rename from view_helpers.go rename to pkg/gui/view_helpers.go index b28e84efb..fc252a25c 100644 --- a/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -1,4 +1,4 @@ -package main +package gui import ( "fmt" @@ -233,3 +233,7 @@ func getCommitMessageView(g *gocui.Gui) *gocui.View { v, _ := g.View("commitMessage") return v } + +func trimmedContent(v *gocui.View) string { + return strings.TrimSpace(v.Buffer()) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 000000000..01556ea4f --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,49 @@ +package utils + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" +) + +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 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) +} diff --git a/utils.go b/utils.go index e2de46233..df7d04818 100644 --- a/utils.go +++ b/utils.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/fatih/color" - "github.com/jesseduffield/gocui" ) func splitLines(multilineString string) []string { @@ -23,10 +22,6 @@ func splitLines(multilineString string) []string { 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 From c01bc094423cf8a0a861ee8b3bd5432cd958339d Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 12 Aug 2018 19:50:55 +1000 Subject: [PATCH 02/19] WIP refactor --- pkg/commands/git.go | 158 ++++++++++++++++++++++++++++++- pkg/commands/gitcommands.go | 74 +-------------- pkg/gui/panels/branches_panel.go | 2 +- pkg/gui/panels/commits_panel.go | 2 +- pkg/gui/panels/files_panel.go | 2 +- pkg/gui/panels/status_panel.go | 2 +- 6 files changed, 159 insertions(+), 81 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 17855b98e..23a7d90f9 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -3,17 +3,23 @@ package commands import ( "fmt" "os" + "strings" + "time" "github.com/Sirupsen/logrus" - git "gopkg.in/src-d/go-git.v4" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/git" + gitconfig "github.com/tcnksm/go-gitconfig" + gogit "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/object" ) // GitCommand is our main git interface type GitCommand struct { Log *logrus.Logger OSCommand *OSCommand - Worktree *git.Worktree - Repo *git.Repository + Worktree *gogit.Worktree + Repo *gogit.Repository } // NewGitCommand it runs git commands @@ -32,6 +38,7 @@ func (c *GitCommand) SetupGit() { c.setupWorktree() } +// GitIgnore adds a file to the .gitignore of the repo func (c *GitCommand) GitIgnore(filename string) { if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { panic(err) @@ -55,7 +62,6 @@ func (c *GitCommand) navigateToRepoRootDirectory() { } func (c *GitCommand) setupWorktree() { - var err error r, err := git.PlainOpen(".") if err != nil { panic(err) @@ -63,9 +69,151 @@ func (c *GitCommand) setupWorktree() { c.Repo = r w, err := r.Worktree() - c.Worktree = w 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: git.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 splitLines(pushables) +} + +// GetGitBranches returns a list of branches for the current repo, with recency +// values stored against those that are in the reflog +func (c *GitCommand) GetGitBranches() []Branch { + builder := git.newBranchListBuilder() + return builder.build() +} + +// BranchIncluded states whether a branch is included in a list of branches, +// with a case insensitive comparison on name +func (c *GitCommand) BranchIncluded(branchName string, branches []Branch) bool { + for _, existingBranch := range branches { + if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { + return true + } + } + return false +} + +// 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 + "\"") +} + +func (c *GitCommand) Fetch() (string, error) { + return c.OSCommand.runDirectCommand("git fetch") +} + +func (c *GitCommand) ResetToCommit(sha string) (string, error) { + return c.OSCommand.runDirectCommand("git reset " + sha) +} + +func (c *GitCommand) NewBranch(name string) (string, error) { + return c.OSCommand.runDirectCommand("git checkout -b " + name) +} + +func (c *GitCommand) DeleteBranch(branch string) (string, error) { + return runCommand("git branch -d " + branch) +} + +func (c *GitCommand) ListStash() (string, error) { + return c.OSCommand.runDirectCommand("git stash list") +} + +func (c *GitCommand) Merge(branchName string) (string, error) { + return c.OSCommand.runDirectCommand("git merge --no-edit " + branchName) +} + +func (c *GitCommand) AbortMerge() (string, error) { + return c.OSCommand.runDirectCommand("git merge --abort") +} + +func gitCommit(g *gocui.Gui, message string) (string, error) { + gpgsign, _ := gitconfig.Global("commit.gpgsign") + if gpgsign != "" { + runSubProcess(g, "git", "commit") + return "", nil + } + userName, err := gitconfig.Username() + if userName == "" { + return "", errNoUsername + } + userEmail, err := gitconfig.Email() + _, err = w.Commit(message, &git.CommitOptions{ + Author: &object.Signature{ + Name: userName, + Email: userEmail, + When: time.Now(), + }, + }) + if err != nil { + return err.Error(), err + } + return "", nil +} + +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 +} diff --git a/pkg/commands/gitcommands.go b/pkg/commands/gitcommands.go index bdc23fef2..f6eb4a357 100644 --- a/pkg/commands/gitcommands.go +++ b/pkg/commands/gitcommands.go @@ -1,4 +1,4 @@ -package main +package commands import ( @@ -279,7 +279,7 @@ func verifyInGitRepo() { } func getCommits() []Commit { - pushables := gitCommitsToPush() + pushables := git.GetCommitsToPush() log := getLog() commits := make([]Commit, 0) // now we can split it up and turn it into commits @@ -447,73 +447,3 @@ func gitSquashFixupCommit(branchName string, shaValue string) (string, error) { } 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}) -} diff --git a/pkg/gui/panels/branches_panel.go b/pkg/gui/panels/branches_panel.go index a73d28bb3..82b8542ad 100644 --- a/pkg/gui/panels/branches_panel.go +++ b/pkg/gui/panels/branches_panel.go @@ -123,7 +123,7 @@ func refreshBranches(g *gocui.Gui) error { if err != nil { panic(err) } - state.Branches = getGitBranches() + state.Branches = git.GetGitBranches() v.Clear() for _, branch := range state.Branches { fmt.Fprintln(v, branch.getDisplayString()) diff --git a/pkg/gui/panels/commits_panel.go b/pkg/gui/panels/commits_panel.go index 1a9976ffe..5b02ae115 100644 --- a/pkg/gui/panels/commits_panel.go +++ b/pkg/gui/panels/commits_panel.go @@ -148,7 +148,7 @@ func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { 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 { + if output, err := git.RenameCommit(v.Buffer()); err != nil { return createErrorPanel(g, output) } if err := refreshCommits(g); err != nil { diff --git a/pkg/gui/panels/files_panel.go b/pkg/gui/panels/files_panel.go index 4de8caca7..fb4fc8196 100644 --- a/pkg/gui/panels/files_panel.go +++ b/pkg/gui/panels/files_panel.go @@ -354,7 +354,7 @@ func handleAbortMerge(g *gocui.Gui, v *gocui.View) error { 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 { + if err := git.ResetHard(); err != nil { createErrorPanel(g, err.Error()) } return refreshFiles(g) diff --git a/pkg/gui/panels/status_panel.go b/pkg/gui/panels/status_panel.go index f3fcb8078..a58375ce0 100644 --- a/pkg/gui/panels/status_panel.go +++ b/pkg/gui/panels/status_panel.go @@ -17,7 +17,7 @@ func refreshStatus(g *gocui.Gui) error { // contents end up cleared g.Update(func(*gocui.Gui) error { v.Clear() - pushables, pullables := gitUpstreamDifferenceCount() + pushables, pullables := git.UpstreamDifferenceCount() fmt.Fprint(v, "↑"+pushables+"↓"+pullables) branches := state.Branches if err := updateHasMergeConflictStatus(); err != nil { From e65ddd7b6facfaf3ebc8b022f26066bdf96a28b0 Mon Sep 17 00:00:00 2001 From: Andrei Miulescu Date: Sun, 12 Aug 2018 20:22:20 +1000 Subject: [PATCH 03/19] Move some commands around --- pkg/commands/git.go | 257 ++++++++++++++++++++- pkg/commands/gitcommands.go | 449 ------------------------------------ pkg/commands/os.go | 67 ++++++ 3 files changed, 314 insertions(+), 459 deletions(-) delete mode 100644 pkg/commands/gitcommands.go diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 23a7d90f9..1afe4ae8a 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,8 +1,10 @@ package commands import ( + "errors" "fmt" "os" + "os/exec" "strings" "time" @@ -45,6 +47,114 @@ func (c *GitCommand) GitIgnore(filename string) { } } +// 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 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 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() []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 := !f([]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 +} + +// 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(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 +} + func (c *GitCommand) verifyInGitRepo() { if output, err := c.OSCommand.RunCommand("git status"); err != nil { fmt.Println(output) @@ -127,35 +237,54 @@ 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 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") } -func gitCommit(g *gocui.Gui, message string) (string, error) { +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 + }) +} + +// GitCommit commit to git +func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { runSubProcess(g, "git", "commit") @@ -179,19 +308,25 @@ func gitCommit(g *gocui.Gui, message string) (string, error) { return "", nil } -func gitPull() (string, error) { - return runDirectCommand("git pull --no-edit") +// GitPull pull from repo +func (c *GitCommand) GitPull() (string, error) { + return c.OSCommand.RunCommand("git pull --no-edit") } -func gitPush() (string, error) { - return runDirectCommand("git push -u origin " + state.Branches[0].Name) +// GitPush push to a branch +func (c *GitCommand) GitPush() (string, error) { + return c.OSCommand.RunDirectCommand("git push -u origin " + state.Branches[0].Name) } -func gitSquashPreviousTwoCommits(message string) (string, error) { +// 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 runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"") } -func gitSquashFixupCommit(branchName string, shaValue string) (string, error) { +// 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, @@ -202,7 +337,7 @@ func gitSquashFixupCommit(branchName string, shaValue string) (string, error) { ret := "" for _, command := range commands { devLog(command) - output, err := runDirectCommand(command) + output, err := c.OSCommand.runDirectCommand(command) ret += output if err != nil { devLog(ret) @@ -212,8 +347,110 @@ func gitSquashFixupCommit(branchName string, shaValue string) (string, error) { 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) + 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 GitFile) 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 runs 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(g *gocui.Gui, filename string) { + runSubProcess(g, "git", "add", "--patch", filename) +} + +// 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 +} diff --git a/pkg/commands/gitcommands.go b/pkg/commands/gitcommands.go deleted file mode 100644 index f6eb4a357..000000000 --- a/pkg/commands/gitcommands.go +++ /dev/null @@ -1,449 +0,0 @@ -package commands - -import ( - - // "log" - "errors" - "fmt" - "os" - "os/exec" - "strings" - "time" - - "github.com/jesseduffield/gocui" - gitconfig "github.com/tcnksm/go-gitconfig" - git "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/object" -) - -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") -) - -// 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 := git.GetCommitsToPush() - 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 - } - userName, err := gitconfig.Username() - if userName == "" { - return "", errNoUsername - } - userEmail, err := gitconfig.Email() - _, err = w.Commit(message, &git.CommitOptions{ - Author: &object.Signature{ - Name: userName, - Email: userEmail, - When: time.Now(), - }, - }) - if err != nil { - return err.Error(), err - } - return "", nil -} - -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 -} diff --git a/pkg/commands/os.go b/pkg/commands/os.go index c36990aff..4e7c136a9 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -1,11 +1,20 @@ package commands import ( + "errors" + "os" "os/exec" "runtime" "strings" "github.com/Sirupsen/logrus" + "github.com/jesseduffield/gocui" + 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") ) // Platform stores the os state @@ -75,3 +84,61 @@ func getPlatform() platform { } } } + +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 +func (c *OSCommand) VsCodeOpenFile(g *gocui.Gui, filename string) (string, error) { + return c.runCommand("code -r " + filename) +} + +// SublimeOpenFile opens the filein sublime +// may be deprecated in the future +func (c *OSCommand) SublimeOpenFile(g *gocui.Gui, filename string) (string, error) { + return c.runCommand("subl " + filename) +} + +// OpenFile opens a file with the given +func (c *OSCommand) OpenFile(g *gocui.Gui, filename string) (string, error) { + cmdName, cmdTrail, err := getOpenCommand() + if err != nil { + return "", err + } + return c.runCommand(cmdName + " " + filename + cmdTrail) +} + +// 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(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 := c.OSCommand.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 +} From e8eb78617c17d8b743fcb17c0790c42e11cd5db5 Mon Sep 17 00:00:00 2001 From: Andrei Miulescu Date: Sun, 12 Aug 2018 21:04:47 +1000 Subject: [PATCH 04/19] Mid refactor change some more stuff --- main.go | 21 +-- pkg/commands/git.go | 175 ++++++++++++++++--------- pkg/{git => commands}/git_structs.go | 10 +- pkg/commands/os.go | 27 ++-- pkg/git/branch.go | 6 - pkg/git/branch_list_builder.go | 22 +++- pkg/gui/gui.go | 24 ++++ pkg/gui/panels/branches_panel.go | 5 +- pkg/gui/panels/commit_message_panel.go | 2 +- pkg/gui/panels/commits_panel.go | 2 +- pkg/gui/panels/confirmation_panel.go | 2 +- pkg/gui/panels/files_panel.go | 13 +- pkg/gui/panels/merge_panel.go | 2 +- pkg/gui/panels/stash_panel.go | 2 +- pkg/gui/panels/status_panel.go | 2 +- pkg/utils/utils.go | 23 ++-- 16 files changed, 220 insertions(+), 118 deletions(-) rename pkg/{git => commands}/git_structs.go (81%) diff --git a/main.go b/main.go index aa157c108..a7d5d2eb5 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,22 @@ package main import ( - "errors" "flag" "fmt" "io/ioutil" "log" "os" - "os/exec" "os/user" "path/filepath" "github.com/davecgh/go-spew/spew" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/config" git "gopkg.in/src-d/go-git.v4" ) -// ErrSubProcess is raised when we are running a subprocess var ( - ErrSubprocess = errors.New("running subprocess") - subprocess *exec.Cmd - commit string version = "unversioned" date string @@ -127,17 +120,5 @@ func main() { // TODO remove this once r, w not used setupWorktree() - app.Gui.Run() - - for { - if err := run(); err != nil { - if err == gocui.ErrQuit { - break - } else if err == ErrSubprocess { - subprocess.Run() - } else { - log.Panicln(err) - } - } - } + app.Gui.RunWithSubprocesses() } diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 1afe4ae8a..5bcf2334e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -10,7 +10,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/git" + "github.com/jesseduffield/lazygit/pkg/utils" gitconfig "github.com/tcnksm/go-gitconfig" gogit "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing/object" @@ -51,7 +51,7 @@ func (c *GitCommand) GitIgnore(filename string) { func (c *GitCommand) GetStashEntries() []StashEntry { stashEntries := make([]StashEntry, 0) rawString, _ := c.OSCommand.RunDirectCommand("git stash list --pretty='%gs'") - for i, line := range splitLines(rawString) { + for i, line := range utils.SplitLines(rawString) { stashEntries = append(stashEntries, stashEntryFromLine(line, i)) } return stashEntries @@ -67,7 +67,7 @@ func stashEntryFromLine(line string, index int) StashEntry { // GetStashEntryDiff stash diff func (c *GitCommand) GetStashEntryDiff(index int) (string, error) { - return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") + return c.OSCommand.RunCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") } func includes(array []string, str string) bool { @@ -81,8 +81,8 @@ func includes(array []string, str string) bool { // GetStatusFiles git status files func (c *GitCommand) GetStatusFiles() []GitFile { - statusOutput, _ := getGitStatus() - statusStrings := splitLines(statusOutput) + statusOutput, _ := c.GitStatus() + statusStrings := utils.SplitLines(statusOutput) gitFiles := make([]GitFile, 0) for _, statusString := range statusStrings { @@ -90,7 +90,7 @@ func (c *GitCommand) GetStatusFiles() []GitFile { stagedChange := change[0:1] unstagedChange := statusString[1:2] filename := statusString[3:] - tracked := !f([]string{"??", "A "}, change) + tracked := !includes([]string{"??", "A "}, change) gitFile := GitFile{ Name: filename, DisplayString: statusString, @@ -102,7 +102,7 @@ func (c *GitCommand) GetStatusFiles() []GitFile { } gitFiles = append(gitFiles, gitFile) } - objectLog(gitFiles) + c.Log.Info(gitFiles) // TODO: use a dumper-esque log here return gitFiles } @@ -162,6 +162,11 @@ func (c *GitCommand) verifyInGitRepo() { } } +// 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) { @@ -172,7 +177,7 @@ func (c *GitCommand) navigateToRepoRootDirectory() { } func (c *GitCommand) setupWorktree() { - r, err := git.PlainOpen(".") + r, err := gogit.PlainOpen(".") if err != nil { panic(err) } @@ -187,17 +192,17 @@ func (c *GitCommand) setupWorktree() { // ResetHard does the equivalent of `git reset --hard HEAD` func (c *GitCommand) ResetHard() error { - return c.Worktree.Reset(&gogit.ResetOptions{Mode: git.HardReset}) + 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") + 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") + pullableCount, err := c.OSCommand.RunDirectCommand("git rev-list head..@{u} --count") if err != nil { return "?", "?" } @@ -207,18 +212,11 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) { // 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") + pushables, err := c.OSCommand.RunDirectCommand("git rev-list @{u}..head --abbrev-commit") if err != nil { return make([]string, 0) } - return splitLines(pushables) -} - -// GetGitBranches returns a list of branches for the current repo, with recency -// values stored against those that are in the reflog -func (c *GitCommand) GetGitBranches() []Branch { - builder := git.newBranchListBuilder() - return builder.build() + return utils.SplitLines(pushables) } // BranchIncluded states whether a branch is included in a list of branches, @@ -234,60 +232,49 @@ func (c *GitCommand) BranchIncluded(branchName string, branches []Branch) bool { // 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 + "\"") + 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") + 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) + 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) + return c.OSCommand.RunDirectCommand("git checkout -b " + name) } // DeleteBranch delete branch func (c *GitCommand) DeleteBranch(branch string) (string, error) { - return runCommand("git branch -d " + branch) + return c.OSCommand.RunCommand("git branch -d " + branch) } // ListStash list stash func (c *GitCommand) ListStash() (string, error) { - return c.OSCommand.runDirectCommand("git stash list") + 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) + 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") -} - -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 - }) + return c.OSCommand.RunDirectCommand("git merge --abort") } // GitCommit commit to git func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { - runSubProcess(g, "git", "commit") + sub, err := c.OSCommand.RunSubProcess("git", "commit") return "", nil } userName, err := gitconfig.Username() @@ -295,7 +282,7 @@ func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { return "", errNoUsername } userEmail, err := gitconfig.Email() - _, err = w.Commit(message, &git.CommitOptions{ + _, err = c.Worktree.Commit(message, &gogit.CommitOptions{ Author: &object.Signature{ Name: userName, Email: userEmail, @@ -321,7 +308,7 @@ func (c *GitCommand) GitPush() (string, error) { // 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 runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"") + return c.OSCommand.RunDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"") } // SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it, @@ -336,18 +323,18 @@ func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) (stri } ret := "" for _, command := range commands { - devLog(command) - output, err := c.OSCommand.runDirectCommand(command) + c.Log.Info(command) + output, err := c.OSCommand.RunDirectCommand(command) ret += output if err != nil { - devLog(ret) + 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) + output, _ := c.OSCommand.RunDirectCommand("git branch -d " + shaValue) ret += output output, _ = c.OSCommand.RunDirectCommand("git checkout " + branchName) ret += output @@ -357,12 +344,12 @@ func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) (stri // CatFile obtain the contents of a file func (c *GitCommand) CatFile(file string) (string, error) { - return c.OSCommand.runDirectCommand("cat " + file) + return c.OSCommand.RunDirectCommand("cat " + file) } // StageFile stages a file func (c *GitCommand) StageFile(file string) error { - _, err := c.OSCommand.runCommand("git add " + file) + _, err := c.OSCommand.RunCommand("git add " + file) return err } @@ -374,18 +361,18 @@ func (c *GitCommand) UnStageFile(file string, tracked bool) error { } else { command = "git rm --cached " } - _, err := c.OSCommand.runCommand(command + file) + _, 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") + 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") + output, err := c.OSCommand.RunCommand("git status --untracked-files=all") if err != nil { return false, err } @@ -396,11 +383,11 @@ func (c *GitCommand) IsInMergeState() (bool, error) { func (c *GitCommand) RemoveFile(file GitFile) 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) + _, 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) + _, err := c.OSCommand.RunCommand("git checkout " + file.Name) return err } @@ -410,24 +397,24 @@ func (c *GitCommand) Checkout(branch string, force bool) (string, error) { if force { forceArg = "--force " } - return c.OSCommand.runCommand("git checkout " + forceArg + branch) + return c.OSCommand.RunCommand("git checkout " + forceArg + branch) } // AddPatch runs 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(g *gocui.Gui, filename string) { - runSubProcess(g, "git", "add", "--patch", filename) +func (c *GitCommand) AddPatch(g *gocui.Gui, filename string) (*exec.Cmd, error) { + return c.OSCommand.RunSubProcess("git", "add", "--patch", filename) } // 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) + 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 { +// 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) @@ -454,3 +441,73 @@ func includesInt(list []int, a int) bool { } return false } + +// GetCommits obtains the commits of the current branch +func (c *GitCommand) GetCommits() []Commit { + pushables := gogit.GetCommitsToPush() + log := getLog() + commits := make([]Commit, 0) + // now we can split it up and turn it into commits + lines := utils.RplitLines(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 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, _ := c.OSCommand.RunCommand(command) + return s +} diff --git a/pkg/git/git_structs.go b/pkg/commands/git_structs.go similarity index 81% rename from pkg/git/git_structs.go rename to pkg/commands/git_structs.go index 711f25f4b..dd28d15fa 100644 --- a/pkg/git/git_structs.go +++ b/pkg/commands/git_structs.go @@ -1,8 +1,8 @@ -package git +package commands // File : A staged/unstaged file // TODO: decide whether to give all of these the Git prefix -type File struct { +type GitFile struct { Name string HasStagedChanges bool HasUnstagedChanges bool @@ -26,3 +26,9 @@ type StashEntry struct { Name string DisplayString string } + +// Branch : A git branch +type Branch struct { + Name string + Recency string +} diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 4e7c136a9..e7fe4515f 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -85,7 +85,8 @@ func getPlatform() platform { } } -func (c *OSCommand) getOpenCommand() (string, string, error) { +// 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 &", @@ -93,7 +94,7 @@ func (c *OSCommand) getOpenCommand() (string, string, error) { "open": "", } for name, trail := range trailMap { - if out, _ := c.runCommand("which " + name); out != "exit status 1" { + if out, _ := c.RunCommand("which " + name); out != "exit status 1" { return name, trail, nil } } @@ -103,22 +104,22 @@ func (c *OSCommand) getOpenCommand() (string, string, error) { // VsCodeOpenFile opens the file in code, with the -r flag to open in the // current window func (c *OSCommand) VsCodeOpenFile(g *gocui.Gui, filename string) (string, error) { - return c.runCommand("code -r " + filename) + return c.RunCommand("code -r " + filename) } // SublimeOpenFile opens the filein sublime // may be deprecated in the future func (c *OSCommand) SublimeOpenFile(g *gocui.Gui, filename string) (string, error) { - return c.runCommand("subl " + filename) + return c.RunCommand("subl " + filename) } // OpenFile opens a file with the given func (c *OSCommand) OpenFile(g *gocui.Gui, filename string) (string, error) { - cmdName, cmdTrail, err := getOpenCommand() + cmdName, cmdTrail, err := c.GetOpenCommand() if err != nil { return "", err } - return c.runCommand(cmdName + " " + filename + cmdTrail) + return c.RunCommand(cmdName + " " + filename + cmdTrail) } // EditFile opens a file in a subprocess using whatever editor is available, @@ -132,13 +133,23 @@ func (c *OSCommand) editFile(g *gocui.Gui, filename string) (string, error) { editor = os.Getenv("EDITOR") } if editor == "" { - if _, err := c.OSCommand.runCommand("which vi"); err == nil { + if _, err := c.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) + c.RunSubProcess(editor, filename) return "", nil } + +// RunSubProcess iniRunSubProcessrocess then tells the Gui to switch to it +func (c *OSCommand) RunSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) { + subprocess := exec.Command(cmdName, commandArgs...) + subprocess.Stdin = os.Stdin + subprocess.Stdout = os.Stdout + subprocess.Stderr = os.Stderr + + return subprocess, nil +} diff --git a/pkg/git/branch.go b/pkg/git/branch.go index e0fd75a73..78a52bbc8 100644 --- a/pkg/git/branch.go +++ b/pkg/git/branch.go @@ -6,12 +6,6 @@ import ( "github.com/fatih/color" ) -// Branch : A git branch -type Branch struct { - Name string - Recency string -} - // GetDisplayString returns the dispaly string of branch // func (b *Branch) GetDisplayString() string { // return gui.withPadding(b.Recency, 4) + gui.coloredString(b.Name, b.getColor()) diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go index 1abf11fcc..2f80dba32 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -4,6 +4,10 @@ import ( "regexp" "strings" + "github.com/jesseduffield/lazygit/pkg/commands" + + "github.com/Sirupsen/logrus" + "gopkg.in/src-d/go-git.v4/plumbing" ) @@ -15,20 +19,26 @@ import ( // our safe branches, then add the remaining safe branches, ensuring uniqueness // along the way -type branchListBuilder struct{} - -func newBranchListBuilder() *branchListBuilder { - return &branchListBuilder{} +type BranchListBuilder struct { + Log *logrus.Log + GitCommand *commands.GitCommand } -func (b *branchListBuilder) obtainCurrentBranch() Branch { +func NewBranchListBuilder(log *logrus.Logger, gitCommand *GitCommand) (*BranchListBuilder, error) { + return nil, &BranchListBuilder{ + Log: log, + GitCommand: gitCommand + } +} + +func (b *branchListBuilder) ObtainCurrentBranch() Branch { // I used go-git for this, but that breaks if you've just done a git init, // even though you're on 'master' branchName, _ := runDirectCommand("git symbolic-ref --short HEAD") return Branch{Name: strings.TrimSpace(branchName), Recency: " *"} } -func (*branchListBuilder) obtainReflogBranches() []Branch { +func (*branchListBuilder) ObtainReflogBranches() []Branch { branches := make([]Branch, 0) rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") if err != nil { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index f35a4811f..8c455d369 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -5,6 +5,9 @@ import ( // "io" // "io/ioutil" + "errors" + "log" + "os/exec" "runtime" "strings" "time" @@ -19,6 +22,13 @@ import ( // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false +// ErrSubprocess tells us we're switching to a subprocess so we need to +// close the Gui until it is finished +var ( + ErrSubprocess = errors.New("running subprocess") + subprocess *exec.Cmd +) + type stateType struct { GitFiles []git.File Branches []git.Branch @@ -273,6 +283,20 @@ func resizePopupPanels(g *gocui.Gui) error { return nil } +func RunWithSubprocesses() { + for { + if err := run(); err != nil { + if err == gocui.ErrQuit { + break + } else if err == ErrSubprocess { + subprocess.Run() + } else { + log.Panicln(err) + } + } + } +} + func run() (err error) { g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) if err != nil { diff --git a/pkg/gui/panels/branches_panel.go b/pkg/gui/panels/branches_panel.go index 82b8542ad..63e3ad7e8 100644 --- a/pkg/gui/panels/branches_panel.go +++ b/pkg/gui/panels/branches_panel.go @@ -1,4 +1,4 @@ -package main +package panels import ( "fmt" @@ -123,7 +123,8 @@ func refreshBranches(g *gocui.Gui) error { if err != nil { panic(err) } - state.Branches = git.GetGitBranches() + builder := git.newBranchListBuilder() // TODO: add constructor params + state.Branches = builder.build() v.Clear() for _, branch := range state.Branches { fmt.Fprintln(v, branch.getDisplayString()) diff --git a/pkg/gui/panels/commit_message_panel.go b/pkg/gui/panels/commit_message_panel.go index baef870cf..12634c574 100644 --- a/pkg/gui/panels/commit_message_panel.go +++ b/pkg/gui/panels/commit_message_panel.go @@ -1,4 +1,4 @@ -package main +package panels import "github.com/jesseduffield/gocui" diff --git a/pkg/gui/panels/commits_panel.go b/pkg/gui/panels/commits_panel.go index 5b02ae115..96f897d96 100644 --- a/pkg/gui/panels/commits_panel.go +++ b/pkg/gui/panels/commits_panel.go @@ -1,4 +1,4 @@ -package main +package panels import ( "errors" diff --git a/pkg/gui/panels/confirmation_panel.go b/pkg/gui/panels/confirmation_panel.go index a8719d237..776504e66 100644 --- a/pkg/gui/panels/confirmation_panel.go +++ b/pkg/gui/panels/confirmation_panel.go @@ -4,7 +4,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package main +package panels import ( "strings" diff --git a/pkg/gui/panels/files_panel.go b/pkg/gui/panels/files_panel.go index fb4fc8196..12dfde91e 100644 --- a/pkg/gui/panels/files_panel.go +++ b/pkg/gui/panels/files_panel.go @@ -1,4 +1,4 @@ -package main +package panels import ( @@ -194,6 +194,17 @@ func handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error { return nil } +func runSubprocess(g *gocui.Gui, commands string...) error { + var err error + // need this OsCommand to be available + if subprocess, err = osCommand.RunSubProcess(commands...); err != nil { + return err + } + g.Update(func(g *gocui.Gui) error { + return gui.ErrSubprocess + }) +} + func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error { file, err := getSelectedFile(g) if err != nil { diff --git a/pkg/gui/panels/merge_panel.go b/pkg/gui/panels/merge_panel.go index f5ca12a23..e15608829 100644 --- a/pkg/gui/panels/merge_panel.go +++ b/pkg/gui/panels/merge_panel.go @@ -1,6 +1,6 @@ // 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 +package panels import ( "bufio" diff --git a/pkg/gui/panels/stash_panel.go b/pkg/gui/panels/stash_panel.go index 33c7e297b..045bf3794 100644 --- a/pkg/gui/panels/stash_panel.go +++ b/pkg/gui/panels/stash_panel.go @@ -1,4 +1,4 @@ -package main +package panels import ( "fmt" diff --git a/pkg/gui/panels/status_panel.go b/pkg/gui/panels/status_panel.go index a58375ce0..673bad802 100644 --- a/pkg/gui/panels/status_panel.go +++ b/pkg/gui/panels/status_panel.go @@ -1,4 +1,4 @@ -package main +package panels import ( "fmt" diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 01556ea4f..6b91ed6ef 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -10,7 +10,10 @@ import ( "github.com/fatih/color" ) -func splitLines(multilineString string) []string { +// 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) @@ -22,25 +25,29 @@ func splitLines(multilineString string) []string { return lines } -func withPadding(str string, padding int) string { +// 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)) } -func coloredString(str string, colorAttribute color.Attribute) string { +// 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) + 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 { +// 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)) } -// used to get the project name -func getCurrentProject() string { +// GetCurrentProject gets the repo's base name +func GetCurrentProject() string { pwd, err := os.Getwd() if err != nil { log.Fatalln(err.Error()) From e6beb5d50b40640fd83b55bc44fa377408de8b05 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 12 Aug 2018 23:29:58 +1000 Subject: [PATCH 05/19] no more go-git for committing (reflecting the change in master) --- pkg/commands/git.go | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 5bcf2334e..99ecec720 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -6,14 +6,12 @@ import ( "os" "os/exec" "strings" - "time" "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" - "gopkg.in/src-d/go-git.v4/plumbing/object" ) // GitCommand is our main git interface @@ -272,27 +270,13 @@ func (c *GitCommand) AbortMerge() (string, error) { // GitCommit commit to git func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { + command := "git commit -m \"" + message + "\"" gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { sub, err := c.OSCommand.RunSubProcess("git", "commit") return "", nil } - userName, err := gitconfig.Username() - if userName == "" { - return "", errNoUsername - } - userEmail, err := gitconfig.Email() - _, err = c.Worktree.Commit(message, &gogit.CommitOptions{ - Author: &object.Signature{ - Name: userName, - Email: userEmail, - When: time.Now(), - }, - }) - if err != nil { - return err.Error(), err - } - return "", nil + return c.OSCommand.RunDirectCommand(command) } // GitPull pull from repo From f9c39ad64bddd1577636c0ce5606eda44bc704ef Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 17:20:03 +1000 Subject: [PATCH 06/19] add newline to version output --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index a7d5d2eb5..fda30b6fb 100644 --- a/main.go +++ b/main.go @@ -103,7 +103,7 @@ func main() { version = fallbackVersion() } if *versionFlag { - fmt.Printf("commit=%s, build date=%s, version=%s", commit, date, version) + fmt.Printf("commit=%s, build date=%s, version=%s\n", commit, date, version) os.Exit(0) } appConfig := &config.AppConfig{ From 97cff656121270e9c790432e28622d92ab7b0f1a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 20:26:02 +1000 Subject: [PATCH 07/19] progress on refactor --- pkg/app/app.go | 7 + pkg/{git => commands}/branch.go | 16 +- pkg/commands/git.go | 92 +++-- pkg/commands/git_structs.go | 12 +- pkg/commands/os.go | 10 +- pkg/git/branch_list_builder.go | 63 ++-- pkg/gui/branches_panel.go | 141 ++++++++ pkg/gui/commit_message_panel.go | 50 +++ pkg/gui/commits_panel.go | 176 ++++++++++ pkg/gui/{panels => }/confirmation_panel.go | 74 ++-- pkg/gui/files_panel.go | 383 +++++++++++++++++++++ pkg/gui/gui.go | 150 ++++---- pkg/gui/keybindings.go | 102 +++--- pkg/gui/{panels => }/merge_panel.go | 38 +- pkg/gui/panels/branches_panel.go | 136 -------- pkg/gui/panels/commit_message_panel.go | 45 --- pkg/gui/panels/commits_panel.go | 176 ---------- pkg/gui/panels/files_panel.go | 373 -------------------- pkg/gui/{panels => }/stash_panel.go | 28 +- pkg/gui/{panels => }/status_panel.go | 2 +- pkg/gui/view_helpers.go | 58 ++-- pkg/utils/utils.go | 9 + utils.go | 49 --- 23 files changed, 1104 insertions(+), 1086 deletions(-) rename pkg/{git => commands}/branch.go (61%) create mode 100644 pkg/gui/branches_panel.go create mode 100644 pkg/gui/commit_message_panel.go create mode 100644 pkg/gui/commits_panel.go rename pkg/gui/{panels => }/confirmation_panel.go (53%) create mode 100644 pkg/gui/files_panel.go rename pkg/gui/{panels => }/merge_panel.go (84%) delete mode 100644 pkg/gui/panels/branches_panel.go delete mode 100644 pkg/gui/panels/commit_message_panel.go delete mode 100644 pkg/gui/panels/commits_panel.go delete mode 100644 pkg/gui/panels/files_panel.go rename pkg/gui/{panels => }/stash_panel.go (69%) rename pkg/gui/{panels => }/status_panel.go (98%) delete mode 100644 utils.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 726567b01..b6318b745 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -4,8 +4,10 @@ import ( "io" "github.com/Sirupsen/logrus" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui" ) // App struct @@ -16,6 +18,7 @@ type App struct { Log *logrus.Logger OSCommand *commands.OSCommand GitCommand *commands.GitCommand + Gui *gocui.Gui } // NewApp retruns a new applications @@ -34,6 +37,10 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return nil, err } + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, config.GetVersion()) + if err != nil { + return nil, err + } return app, nil } diff --git a/pkg/git/branch.go b/pkg/commands/branch.go similarity index 61% rename from pkg/git/branch.go rename to pkg/commands/branch.go index 78a52bbc8..13c26e766 100644 --- a/pkg/git/branch.go +++ b/pkg/commands/branch.go @@ -1,15 +1,23 @@ -package git +package commands import ( "strings" "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/utils" ) +// Branch : A git branch +// duplicating this for now +type Branch struct { + Name string + Recency string +} + // GetDisplayString returns the dispaly string of branch -// func (b *Branch) GetDisplayString() string { -// return gui.withPadding(b.Recency, 4) + gui.coloredString(b.Name, b.getColor()) -// } +func (b *Branch) GetDisplayString() string { + return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor()) +} // GetColor branch color func (b *Branch) GetColor() color.Attribute { diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 99ecec720..016a08fc6 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -38,13 +38,6 @@ func (c *GitCommand) SetupGit() { c.setupWorktree() } -// GitIgnore adds a file to the .gitignore of the repo -func (c *GitCommand) GitIgnore(filename string) { - if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil { - panic(err) - } -} - // GetStashEntries stash entryies func (c *GitCommand) GetStashEntries() []StashEntry { stashEntries := make([]StashEntry, 0) @@ -78,10 +71,10 @@ func includes(array []string, str string) bool { } // GetStatusFiles git status files -func (c *GitCommand) GetStatusFiles() []GitFile { +func (c *GitCommand) GetStatusFiles() []File { statusOutput, _ := c.GitStatus() statusStrings := utils.SplitLines(statusOutput) - gitFiles := make([]GitFile, 0) + files := make([]File, 0) for _, statusString := range statusStrings { change := statusString[0:2] @@ -89,7 +82,7 @@ func (c *GitCommand) GetStatusFiles() []GitFile { unstagedChange := statusString[1:2] filename := statusString[3:] tracked := !includes([]string{"??", "A "}, change) - gitFile := GitFile{ + file := File{ Name: filename, DisplayString: statusString, HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange), @@ -98,10 +91,10 @@ func (c *GitCommand) GetStatusFiles() []GitFile { Deleted: unstagedChange == "D" || stagedChange == "D", HasMergeConflicts: change == "UU", } - gitFiles = append(gitFiles, gitFile) + files = append(files, file) } - c.Log.Info(gitFiles) // TODO: use a dumper-esque log here - return gitFiles + c.Log.Info(files) // TODO: use a dumper-esque log here + return files } // StashDo modify stash @@ -124,19 +117,19 @@ func (c *GitCommand) StashSave(message string) (string, error) { } // MergeStatusFiles merge status files -func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { - if len(oldGitFiles) == 0 { - return newGitFiles +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([]GitFile, 0) - for _, oldGitFile := range oldGitFiles { - for newIndex, newGitFile := range newGitFiles { - if oldGitFile.Name == newGitFile.Name { - result = append(result, newGitFile) + 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 } @@ -144,9 +137,9 @@ func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitF } // append any new files to the end - for index, newGitFile := range newGitFiles { + for index, newFile := range newFiles { if !includesInt(appendedIndexes, index) { - result = append(result, newGitFile) + result = append(result, newFile) } } @@ -217,17 +210,6 @@ func (c *GitCommand) GetCommitsToPush() []string { return utils.SplitLines(pushables) } -// BranchIncluded states whether a branch is included in a list of branches, -// with a case insensitive comparison on name -func (c *GitCommand) BranchIncluded(branchName string, branches []Branch) bool { - for _, existingBranch := range branches { - if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { - return true - } - } - return false -} - // 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 + "\"") @@ -268,25 +250,26 @@ func (c *GitCommand) AbortMerge() (string, error) { return c.OSCommand.RunDirectCommand("git merge --abort") } -// GitCommit commit to git -func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { +// Commit commit to git +func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { command := "git commit -m \"" + message + "\"" gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { - sub, err := c.OSCommand.RunSubProcess("git", "commit") - return "", nil + return c.OSCommand.PrepareSubProcess("git", "commit") } - return c.OSCommand.RunDirectCommand(command) + // TODO: make these runDirectCommand functions just return an error + _, err := c.OSCommand.RunDirectCommand(command) + return nil, err } -// GitPull pull from repo -func (c *GitCommand) GitPull() (string, error) { +// Pull pull from repo +func (c *GitCommand) Pull() (string, error) { return c.OSCommand.RunCommand("git pull --no-edit") } -// GitPush push to a branch -func (c *GitCommand) GitPush() (string, error) { - return c.OSCommand.RunDirectCommand("git push -u origin " + state.Branches[0].Name) +// 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 @@ -364,7 +347,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) { } // RemoveFile directly -func (c *GitCommand) RemoveFile(file GitFile) error { +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) @@ -384,10 +367,15 @@ func (c *GitCommand) Checkout(branch string, force bool) (string, error) { return c.OSCommand.RunCommand("git checkout " + forceArg + branch) } -// AddPatch runs a subprocess for adding a patch by patch +// 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(g *gocui.Gui, filename string) (*exec.Cmd, error) { - return c.OSCommand.RunSubProcess("git", "add", "--patch", filename) +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 @@ -428,11 +416,11 @@ func includesInt(list []int, a int) bool { // GetCommits obtains the commits of the current branch func (c *GitCommand) GetCommits() []Commit { - pushables := gogit.GetCommitsToPush() - log := getLog() + pushables := c.GetCommitsToPush() + log := c.GetLog() commits := make([]Commit, 0) // now we can split it up and turn it into commits - lines := utils.RplitLines(log) + lines := utils.SplitLines(log) for _, line := range lines { splitLine := strings.Split(line, " ") sha := splitLine[0] @@ -477,7 +465,7 @@ func (c *GitCommand) Show(sha string) string { } // Diff returns the diff of a file -func (c *GitCommand) Diff(file GitFile) string { +func (c *GitCommand) Diff(file File) string { cachedArg := "" if file.HasStagedChanges && !file.HasUnstagedChanges { cachedArg = "--cached " diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go index dd28d15fa..2f7255be1 100644 --- a/pkg/commands/git_structs.go +++ b/pkg/commands/git_structs.go @@ -2,7 +2,7 @@ package commands // File : A staged/unstaged file // TODO: decide whether to give all of these the Git prefix -type GitFile struct { +type File struct { Name string HasStagedChanges bool HasUnstagedChanges bool @@ -27,8 +27,10 @@ type StashEntry struct { DisplayString string } -// Branch : A git branch -type Branch struct { - Name string - Recency 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 } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index e7fe4515f..2313b5550 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -15,6 +15,8 @@ import ( 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 @@ -138,14 +140,14 @@ func (c *OSCommand) editFile(g *gocui.Gui, filename string) (string, error) { } } if editor == "" { - return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.") + return "", ErrNoEditorDefined } - c.RunSubProcess(editor, filename) + c.PrepareSubProcess(editor, filename) return "", nil } -// RunSubProcess iniRunSubProcessrocess then tells the Gui to switch to it -func (c *OSCommand) RunSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) { +// 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...) subprocess.Stdin = os.Stdin subprocess.Stdout = os.Stdout diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go index 2f80dba32..faa073119 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/Sirupsen/logrus" @@ -19,59 +20,61 @@ import ( // our safe branches, then add the remaining safe branches, ensuring uniqueness // along the way +// BranchListBuilder returns a list of Branch objects for the current repo type BranchListBuilder struct { - Log *logrus.Log + Log *logrus.Logger GitCommand *commands.GitCommand } -func NewBranchListBuilder(log *logrus.Logger, gitCommand *GitCommand) (*BranchListBuilder, error) { - return nil, &BranchListBuilder{ - Log: log, - GitCommand: gitCommand - } +// 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() Branch { +func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch { // I used go-git for this, but that breaks if you've just done a git init, // even though you're on 'master' - branchName, _ := runDirectCommand("git symbolic-ref --short HEAD") - return Branch{Name: strings.TrimSpace(branchName), Recency: " *"} + branchName, _ := b.GitCommand.OSCommand.RunDirectCommand("git symbolic-ref --short HEAD") + return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"} } -func (*branchListBuilder) ObtainReflogBranches() []Branch { - branches := make([]Branch, 0) - rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") +func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch { + branches := make([]commands.Branch, 0) + rawString, err := b.GitCommand.OSCommand.RunDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") if err != nil { return branches } - branchLines := splitLines(rawString) + branchLines := utils.SplitLines(rawString) for _, line := range branchLines { timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeUnit = abbreviatedTimeUnit(timeUnit) - branch := Branch{Name: branchName, Recency: timeNumber + timeUnit} + branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} branches = append(branches, branch) } return branches } -func (b *branchListBuilder) obtainSafeBranches() []Branch { - branches := make([]Branch, 0) +func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch { + branches := make([]commands.Branch, 0) - bIter, err := r.Branches() + bIter, err := b.GitCommand.Repo.Branches() if err != nil { panic(err) } err = bIter.ForEach(func(b *plumbing.Reference) error { name := b.Name().Short() - branches = append(branches, Branch{Name: name}) + branches = append(branches, commands.Branch{Name: name}) return nil }) 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 { if included == branchIncluded(newBranch.Name, existingBranches) { finalBranches = append(finalBranches, newBranch) @@ -80,7 +83,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi return finalBranches } -func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string { +func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string { for _, safeBranch := range safeBranches { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { return safeBranch.Name @@ -89,15 +92,16 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string { return reflogBranch.Name } -func (b *branchListBuilder) build() []Branch { - branches := make([]Branch, 0) +// Build the list of branches for the current repo +func (b *BranchListBuilder) Build() []commands.Branch { + branches := make([]commands.Branch, 0) head := b.obtainCurrentBranch() safeBranches := b.obtainSafeBranches() if len(safeBranches) == 0 { return append(branches, head) } reflogBranches := b.obtainReflogBranches() - reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...)) + reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...)) for i, reflogBranch := range reflogBranches { reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches) } @@ -108,8 +112,17 @@ func (b *branchListBuilder) build() []Branch { return branches } -func uniqueByName(branches []Branch) []Branch { - finalBranches := make([]Branch, 0) +func branchIncluded(branchName string, branches []commands.Branch) bool { + 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 { if branchIncluded(branch.Name, finalBranches) { continue diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go new file mode 100644 index 000000000..53b8465cb --- /dev/null +++ b/pkg/gui/branches_panel.go @@ -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 +} + +// 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 refreshStatus(g) + }) + return nil +} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go new file mode 100644 index 000000000..f765ab308 --- /dev/null +++ b/pkg/gui/commit_message_panel.go @@ -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") +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go new file mode 100644 index 000000000..45134e44a --- /dev/null +++ b/pkg/gui/commits_panel.go @@ -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) + } + 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) + } + 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 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 +} diff --git a/pkg/gui/panels/confirmation_panel.go b/pkg/gui/confirmation_panel.go similarity index 53% rename from pkg/gui/panels/confirmation_panel.go rename to pkg/gui/confirmation_panel.go index 776504e66..3bec18419 100644 --- a/pkg/gui/panels/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -4,39 +4,40 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package panels +package gui import ( "strings" "github.com/fatih/color" "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 { if function != nil { if err := function(g, v); err != nil { 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") if err != nil { panic(err) } - if err := returnFocus(g, view); err != nil { + if err := gui.returnFocus(g, view); err != nil { panic(err) } g.DeleteKeybindings("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") lineCount := 0 for _, line := range lines { @@ -45,20 +46,20 @@ func getMessageHeight(message string, width int) int { 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() panelWidth := width / 2 - panelHeight := getMessageHeight(prompt, panelWidth) + panelHeight := gui.getMessageHeight(prompt, panelWidth) return width/2 - panelWidth/2, height/2 - panelHeight/2 - panelHeight%2 - 1, width/2 + panelWidth/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") // 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 err != gocui.ErrUnknownView { return err @@ -66,41 +67,41 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand confirmationView.Editable = true confirmationView.Title = title - switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, nil) + gui.switchFocus(g, currentView, confirmationView) + return gui.setKeyBindings(g, handleConfirm, 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.Update(func(g *gocui.Gui) error { // delete the existing confirmation panel if it exists if view, _ := g.View("confirmation"); view != nil { - if err := closeConfirmationPrompt(g); err != nil { + if err := gui.closeConfirmationPrompt(g); err != nil { 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 err != gocui.ErrUnknownView { return err } confirmationView.Title = title confirmationView.FgColor = gocui.ColorWhite - renderString(g, "confirmation", prompt) - switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, handleClose) + gui.renderString(g, "confirmation", prompt) + gui.switchFocus(g, currentView, confirmationView) + return gui.setKeyBindings(g, handleConfirm, handleClose) } 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 // 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 != gocui.ErrUnknownView { return err @@ -111,45 +112,38 @@ func handleNewline(g *gocui.Gui, v *gocui.View) error { return nil } -func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - renderString(g, "options", "esc: close, enter: confirm") - if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil { +func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { + gui.renderString(g, "options", "esc: close, enter: confirm") + if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { 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 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 { - return createConfirmationPanel(g, currentView, title, prompt, nil, nil) +func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error { + 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() colorFunction := color.New(color.FgRed).SprintFunc() 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 { - if strings.HasSuffix(str, "\n") { - return str[:len(str)-1] - } - return str -} - -func resizePopupPanel(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { // If the confirmation panel is already displayed, just resize the width, // otherwise continue - content := trimTrailingNewline(v.Buffer()) - x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content) + content := utils.TrimTrailingNewline(v.Buffer()) + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content) vx0, vy0, vx1, vy1 := v.Dimensions() if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { return nil } - devLog("resizing popup panel") + gui.Log.Info("resizing popup panel") _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0) return err } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go new file mode 100644 index 000000000..9ffcafa44 --- /dev/null +++ b/pkg/gui/files_panel.go @@ -0,0 +1,383 @@ +package gui + +import ( + + // "io" + // "io/ioutil" + + // "strings" + + "errors" + "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 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(*gocui.Gui, string) (string, error)) error { + file, err := gui.getSelectedFile(g) + if err != nil { + if err != errNoFiles { + return err + } + return nil + } + if _, err := open(g, file.Name); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + return nil +} + +func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error { + return gui.genericFileOpen(g, v, gui.editFile) +} + +func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error { + return gui.genericFileOpen(g, v, gui.openFile) +} + +func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { + return gui.genericFileOpen(g, v, gui.sublimeOpenFile) +} + +func (gui *Gui) handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { + return gui.genericFileOpen(g, v, gui.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 := getGitStatusFiles() + gui.State.Files = mergeGitStatusFiles(gui.State.Files, files) + updateHasMergeConflictStatus() +} + +func (gui *Gui) updateHasMergeConflictStatus() error { + merging, err := 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 := 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 + } + refreshStateFiles() + filesView.Clear() + for _, file := range gui.State.Files { + renderFile(file, filesView) + } + correctCursor(filesView) + if filesView == g.CurrentView() { + gui.handleFileSelect(g, filesView) + } + return nil +} + +func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { + createMessagePanel(g, v, "", "Pulling...") + go func() { + if output, err := gitPull(); err != nil { + gui.createErrorPanel(g, output) + } else { + gui.closeConfirmationPrompt(g) + refreshCommits(g) + refreshStatus(g) + } + gui.refreshFiles(g) + }() + return nil +} + +func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { + createMessagePanel(g, v, "", "Pushing...") + go func() { + branchName = gui.State.Branches[0].Name + if output, err := commands.Push(branchName); err != nil { + gui.createErrorPanel(g, output) + } else { + gui.closeConfirmationPrompt(g) + refreshCommits(g) + 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 refreshMergePanel(g) +} + +func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error { + output, err := gitAbortMerge() + if err != nil { + return gui.createErrorPanel(g, output) + } + createMessagePanel(g, v, "", "Merge aborted") + 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 := commands.ResetHard(); err != nil { + gui.createErrorPanel(g, err.Error()) + } + return gui.refreshFiles(g) + }, nil) +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 8c455d369..402b4ff32 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -14,51 +14,69 @@ import ( // "strings" + "github.com/Sirupsen/logrus" "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/git" + "github.com/jesseduffield/lazygit/pkg/commands" ) // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false -// ErrSubprocess tells us we're switching to a subprocess so we need to +// ErrSubProcess tells us we're switching to a subprocess so we need to // close the Gui until it is finished var ( - ErrSubprocess = errors.New("running subprocess") - subprocess *exec.Cmd + ErrSubProcess = errors.New("running subprocess") ) -type stateType struct { - GitFiles []git.File - Branches []git.Branch - Commits []git.Commit - StashEntries []git.StashEntry +// Gui wraps the gocui Gui object which handles rendering and events +type Gui struct { + Gui *gocui.Gui + Log *logrus.Logger + GitCommand *commands.GitCommand + OSCommand *commands.OSCommand + Version string + SubProcess *exec.Cmd + State StateType +} + +// NewGui builds a new gui handler +func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, version string) (*Gui, error) { + initialState := StateType{ + Files: make([]commands.File, 0), + PreviousView: "files", + Commits: make([]commands.Commit, 0), + StashEntries: make([]commands.StashEntry, 0), + ConflictIndex: 0, + ConflictTop: true, + Conflicts: make([]commands.Conflict, 0), + EditHistory: stack.New(), + Platform: getPlatform(), + Version: "test version", // TODO: send version in + } + + return &Gui{ + Log: log, + GitCommand: gitCommand, + OSCommand: oSCommand, + Version: version, + State: initialState, + }, nil +} + +type StateType struct { + Files []commands.File + Branches []commands.Branch + Commits []commands.Commit + StashEntries []commands.StashEntry PreviousView string HasMergeConflicts bool ConflictIndex int ConflictTop bool - Conflicts []conflict + Conflicts []commands.Conflict EditHistory *stack.Stack Platform platform -} - -type conflict struct { - start int - middle int - end int -} - -var state = stateType{ - GitFiles: make([]GitFile, 0), - PreviousView: "files", - Commits: make([]Commit, 0), - StashEntries: make([]StashEntry, 0), - ConflictIndex: 0, - ConflictTop: true, - Conflicts: make([]conflict, 0), - EditHistory: stack.New(), - Platform: getPlatform(), + Version string } type platform struct { @@ -117,7 +135,7 @@ func max(a, b int) int { } // 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.SelFgColor = gocui.ColorWhite | gocui.AttrBold width, height := g.Size() @@ -206,7 +224,7 @@ func layout(g *gocui.Gui) error { 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 { return err } @@ -214,7 +232,7 @@ func layout(g *gocui.Gui) error { v.Frame = false } - if getCommitMessageView(g) == nil { + if gui.getCommitMessageView(g) == nil { // 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 err != gocui.ErrUnknownView { @@ -227,18 +245,18 @@ 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 { return err } v.BgColor = gocui.ColorDefault v.FgColor = gocui.ColorGreen v.Frame = false - renderString(g, "version", version) + gui.renderString(g, "version", gui.Version) // these are only called once - handleFileSelect(g, filesView) - refreshFiles(g) + gui.handleFileSelect(g, filesView) + gui.refreshFiles(g) refreshBranches(g) refreshCommits(g) refreshStashEntries(g) @@ -258,10 +276,10 @@ func fetch(g *gocui.Gui) error { func updateLoader(g *gocui.Gui) error { if confirmationView, _ := g.View("confirmation"); confirmationView != nil { - content := trimmedContent(confirmationView) + content := gui.trimmedContent(confirmationView) if strings.Contains(content, "...") { staticContent := strings.Split(content, "...")[0] + "..." - renderString(g, "confirmation", staticContent+" "+loader()) + gui.renderString(g, "confirmation", staticContent+" "+loader()) } } return nil @@ -283,13 +301,40 @@ func resizePopupPanels(g *gocui.Gui) error { return nil } -func RunWithSubprocesses() { +// Run setup the gui with keybindings and start the mainloop +func (gui *Gui) Run() (*exec.Cmd, error) { + g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) + if err != nil { + return nil, err + } + defer g.Close() + + g.FgColor = gocui.ColorDefault + + goEvery(g, time.Second*60, fetch) + goEvery(g, time.Second*10, gui.refreshFiles) + goEvery(g, time.Millisecond*10, updateLoader) + + g.SetManagerFunc(gui.layout) + + if err = gui.keybindings(g); err != nil { + return nil, err + } + + err = g.MainLoop() + return nil, err +} + +// 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 := run(); err != nil { + if err := gui.Run(); err != nil { if err == gocui.ErrQuit { break - } else if err == ErrSubprocess { - subprocess.Run() + } else if err == ErrSubProcess { + gui.SubProcess.Run() } else { log.Panicln(err) } @@ -297,29 +342,6 @@ func RunWithSubprocesses() { } } -func run() (err error) { - g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) - if err != nil { - return - } - defer g.Close() - - g.FgColor = gocui.ColorDefault - - goEvery(g, time.Second*60, fetch) - goEvery(g, time.Second*10, refreshFiles) - goEvery(g, time.Millisecond*10, updateLoader) - - g.SetManagerFunc(layout) - - if err = keybindings(g); err != nil { - return - } - - err = g.MainLoop() - return -} - func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 23f320ceb..194746d28 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -12,58 +12,58 @@ type Binding struct { Modifier gocui.Modifier } -func keybindings(g *gocui.Gui) error { +func (gui *Gui) keybindings(g *gocui.Gui) error { bindings := []Binding{ - {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, - {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, - {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, - {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, - {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, - {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, - {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, - {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, - {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress}, - {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress}, - {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove}, - {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge}, - {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit}, - {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen}, - {ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, - {ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, - {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, - {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, - {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, - {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge}, - {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch}, - {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard}, - {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, - {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, - {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, - {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop}, - {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, - {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop}, - {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom}, - {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, - {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, - {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, - {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, - {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, - {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch}, - {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, - {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, - {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, - {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, - {ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup}, - {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, - {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop}, - {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, - {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm}, - {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose}, - {ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage}, + {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit}, + {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, + {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, + {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, + {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles}, + {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles}, + {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, + {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress}, + {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress}, + {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, + {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove}, + {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge}, + {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit}, + {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen}, + {ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen}, + {ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen}, + {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile}, + {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles}, + {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave}, + {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge}, + {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch}, + {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard}, + {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge}, + {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk}, + {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks}, + {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, + {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, + {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, + {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, + {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, + {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, + {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, + {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, + {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot}, + {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress}, + {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName}, + {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout}, + {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch}, + {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch}, + {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge}, + {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown}, + {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit}, + {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit}, + {ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup}, + {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply}, + {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop}, + {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop}, + {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm}, + {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose}, + {ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage}, } // Would make these keybindings global but that interferes with editing diff --git a/pkg/gui/panels/merge_panel.go b/pkg/gui/merge_panel.go similarity index 84% rename from pkg/gui/panels/merge_panel.go rename to pkg/gui/merge_panel.go index e15608829..7e0a3a873 100644 --- a/pkg/gui/panels/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -1,6 +1,6 @@ // though this panel is called the merge panel, it's really going to use the main panel. This may change in the future -package panels +package gui import ( "bufio" @@ -12,12 +12,14 @@ import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/utils" ) -func findConflicts(content string) ([]conflict, error) { - conflicts := make([]conflict, 0) +func findConflicts(content string) ([]commands.Conflict, error) { + conflicts := make([]commands.Conflict, 0) var newConflict conflict - for i, line := range splitLines(content) { + for i, line := range utils.SplitLines(content) { if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" { newConflict = conflict{start: i} } else if line == "=======" { @@ -30,15 +32,15 @@ func findConflicts(content string) ([]conflict, error) { return conflicts, nil } -func shiftConflict(conflicts []conflict) (conflict, []conflict) { +func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { return conflicts[0], conflicts[1:] } -func shouldHighlightLine(index int, conflict conflict, top bool) bool { +func 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 coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) { +func coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) { if len(conflicts) == 0 { return content, nil } @@ -87,7 +89,7 @@ func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { return refreshMergePanel(g) } -func isIndexToDelete(i int, conflict conflict, pick string) bool { +func isIndexToDelete(i int, conflict commands.Conflict, pick string) bool { return i == conflict.middle || i == conflict.start || i == conflict.end || @@ -96,8 +98,8 @@ func isIndexToDelete(i int, conflict conflict, pick string) bool { (pick == "top" && i > conflict.middle && i < conflict.end) } -func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error { - gitFile, err := getSelectedFile(g) +func resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error { + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -123,7 +125,7 @@ func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error { } func pushFileSnapshot(g *gocui.Gui) error { - gitFile, err := getSelectedFile(g) + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -140,7 +142,7 @@ func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { return nil } prevContent := state.EditHistory.Pop().(string) - gitFile, err := getSelectedFile(g) + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -204,7 +206,7 @@ func refreshMergePanel(g *gocui.Gui) error { if err := scrollToConflict(g); err != nil { return err } - return renderString(g, "main", content) + return gui.renderString(g, "main", content) } func scrollToConflict(g *gocui.Gui) error { @@ -234,7 +236,7 @@ func switchToMerging(g *gocui.Gui) error { } func renderMergeOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ + return gui.renderOptionsMap(g, map[string]string{ "↑ ↓": "select hunk", "← →": "navigate conflicts", "space": "pick hunk", @@ -248,8 +250,8 @@ func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } - refreshFiles(g) - return switchFocus(g, v, filesView) + gui.refreshFiles(g) + return gui.switchFocus(g, v, filesView) } func handleCompleteMerge(g *gocui.Gui) error { @@ -258,6 +260,6 @@ func handleCompleteMerge(g *gocui.Gui) error { return err } stageSelectedFile(g) - refreshFiles(g) - return switchFocus(g, nil, filesView) + gui.refreshFiles(g) + return gui.switchFocus(g, nil, filesView) } diff --git a/pkg/gui/panels/branches_panel.go b/pkg/gui/panels/branches_panel.go deleted file mode 100644 index 63e3ad7e8..000000000 --- a/pkg/gui/panels/branches_panel.go +++ /dev/null @@ -1,136 +0,0 @@ -package panels - -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) - } - builder := git.newBranchListBuilder() // TODO: add constructor params - state.Branches = builder.build() - v.Clear() - for _, branch := range state.Branches { - fmt.Fprintln(v, branch.getDisplayString()) - } - resetOrigin(v) - return refreshStatus(g) - }) - return nil -} diff --git a/pkg/gui/panels/commit_message_panel.go b/pkg/gui/panels/commit_message_panel.go deleted file mode 100644 index 12634c574..000000000 --- a/pkg/gui/panels/commit_message_panel.go +++ /dev/null @@ -1,45 +0,0 @@ -package panels - -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") -} diff --git a/pkg/gui/panels/commits_panel.go b/pkg/gui/panels/commits_panel.go deleted file mode 100644 index 96f897d96..000000000 --- a/pkg/gui/panels/commits_panel.go +++ /dev/null @@ -1,176 +0,0 @@ -package panels - -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 := git.RenameCommit(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 -} diff --git a/pkg/gui/panels/files_panel.go b/pkg/gui/panels/files_panel.go deleted file mode 100644 index 12dfde91e..000000000 --- a/pkg/gui/panels/files_panel.go +++ /dev/null @@ -1,373 +0,0 @@ -package panels - -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 runSubprocess(g *gocui.Gui, commands string...) error { - var err error - // need this OsCommand to be available - if subprocess, err = osCommand.RunSubProcess(commands...); err != nil { - return err - } - g.Update(func(g *gocui.Gui) error { - return gui.ErrSubprocess - }) -} - -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 := git.ResetHard(); err != nil { - createErrorPanel(g, err.Error()) - } - return refreshFiles(g) - }, nil) -} diff --git a/pkg/gui/panels/stash_panel.go b/pkg/gui/stash_panel.go similarity index 69% rename from pkg/gui/panels/stash_panel.go rename to pkg/gui/stash_panel.go index 045bf3794..1af6bcc29 100644 --- a/pkg/gui/panels/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -1,4 +1,4 @@ -package panels +package gui import ( "fmt" @@ -17,7 +17,7 @@ func refreshStashEntries(g *gocui.Gui) error { for _, stashEntry := range state.StashEntries { fmt.Fprintln(v, stashEntry.DisplayString) } - return resetOrigin(v) + return gui.resetOrigin(v) }) return nil } @@ -26,12 +26,12 @@ func getSelectedStashEntry(v *gocui.View) *StashEntry { if len(state.StashEntries) == 0 { return nil } - lineNumber := getItemPosition(v) + lineNumber := gui.getItemPosition(v) return &state.StashEntries[lineNumber] } func renderStashOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ + return gui.renderOptionsMap(g, map[string]string{ "space": "apply", "g": "pop", "d": "drop", @@ -46,11 +46,11 @@ func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { go func() { stashEntry := getSelectedStashEntry(v) if stashEntry == nil { - renderString(g, "main", "No stash entries") + gui.renderString(g, "main", "No stash entries") return } diff, _ := getStashEntryDiff(stashEntry.Index) - renderString(g, "main", diff) + gui.renderString(g, "main", diff) }() return nil } @@ -64,7 +64,7 @@ func handleStashPop(g *gocui.Gui, v *gocui.View) error { } 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 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 stashDo(g, v, "drop") }, nil) } @@ -72,22 +72,22 @@ func handleStashDrop(g *gocui.Gui, v *gocui.View) error { func stashDo(g *gocui.Gui, v *gocui.View, method string) error { stashEntry := getSelectedStashEntry(v) if stashEntry == nil { - return createErrorPanel(g, "No stash to "+method) + return gui.createErrorPanel(g, "No stash to "+method) } if output, err := gitStashDo(stashEntry.Index, method); err != nil { - createErrorPanel(g, output) + gui.createErrorPanel(g, output) } refreshStashEntries(g) - return refreshFiles(g) + return gui.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) + gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitStashSave(gui.trimmedContent(v)); err != nil { + gui.createErrorPanel(g, output) } refreshStashEntries(g) - return refreshFiles(g) + return gui.refreshFiles(g) }) return nil } diff --git a/pkg/gui/panels/status_panel.go b/pkg/gui/status_panel.go similarity index 98% rename from pkg/gui/panels/status_panel.go rename to pkg/gui/status_panel.go index 673bad802..822be2846 100644 --- a/pkg/gui/panels/status_panel.go +++ b/pkg/gui/status_panel.go @@ -1,4 +1,4 @@ -package panels +package gui import ( "fmt" diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index fc252a25c..a9564de5e 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -11,14 +11,14 @@ import ( var cyclableViews = []string{"files", "branches", "commits", "stash"} -func refreshSidePanels(g *gocui.Gui) error { - refreshBranches(g) - refreshFiles(g) - refreshCommits(g) +func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { + gui.refreshBranches(g) + gui.gui.refreshFiles(g) + gui.refreshCommits(g) 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 if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { focusedViewName = cyclableViews[0] @@ -29,7 +29,7 @@ func nextView(g *gocui.Gui, v *gocui.View) error { break } 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 } } @@ -38,10 +38,10 @@ func nextView(g *gocui.Gui, v *gocui.View) error { if err != nil { panic(err) } - return switchFocus(g, v, focusedView) + return gui.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 if v == nil || v.Name() == cyclableViews[0] { focusedViewName = cyclableViews[len(cyclableViews)-1] @@ -61,16 +61,16 @@ func previousView(g *gocui.Gui, v *gocui.View) error { if err != nil { 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.SetOrigin(0, 0) switch v.Name() { case "files": - return handleFileSelect(g, v) + return gui.handleFileSelect(g, v) case "branches": return handleBranchSelect(g, v) case "confirmation": @@ -91,16 +91,16 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error { } } -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) if err != nil { 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 -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 should never stack confirmation panels if oldView != nil && oldView.Name() != "confirmation" { @@ -117,13 +117,13 @@ func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { return newLineFocused(g, newView) } -func getItemPosition(v *gocui.View) int { +func (gui *Gui) getItemPosition(v *gocui.View) int { _, cy := v.Cursor() _, oy := v.Origin() 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 // TODO: pull this out if v == nil || v.Name() == "main" { @@ -142,7 +142,7 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error { 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 // TODO: pull this out if v == nil || v.Name() == "main" { @@ -163,15 +163,15 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error { return nil } -func resetOrigin(v *gocui.View) error { +func (gui *Gui) resetOrigin(v *gocui.View) error { if err := v.SetCursor(0, 0); err != nil { return err } return v.SetOrigin(0, 0) } -// if the cursor down past the last item, move it up one -func correctCursor(v *gocui.View) error { +// if the cursor down past the last item, move it to the last line +func (gui *Gui) correctCursor(v *gocui.View) error { cx, cy := v.Cursor() _, oy := v.Origin() lineCount := len(v.BufferLines()) - 2 @@ -181,7 +181,7 @@ func correctCursor(v *gocui.View) error { 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 { v, err := g.View(viewName) // just in case the view disappeared as this function was called, we'll @@ -197,7 +197,7 @@ func renderString(g *gocui.Gui, viewName, s string) error { return nil } -func optionsMapToString(optionsMap map[string]string) string { +func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { optionsArray := make([]string, 0) for key, description := range optionsMap { optionsArray = append(optionsArray, key+": "+description) @@ -206,11 +206,11 @@ func optionsMapToString(optionsMap map[string]string) string { return strings.Join(optionsArray, ", ") } -func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { - return renderString(g, "options", optionsMapToString(optionsMap)) +func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { + return gui.renderString(g, "options", optionsMapToString(optionsMap)) } -func loader() string { +func (gui *Gui) loader() string { characters := "|/-\\" now := time.Now() nanos := now.UnixNano() @@ -219,21 +219,21 @@ func loader() string { } // TODO: refactor properly -func getFilesView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { v, _ := g.View("files") return v } -func getCommitsView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View { v, _ := g.View("commits") return v } -func getCommitMessageView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View { v, _ := g.View("commitMessage") return v } -func trimmedContent(v *gocui.View) string { +func (gui *Gui) trimmedContent(v *gocui.View) string { return strings.TrimSpace(v.Buffer()) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6b91ed6ef..b3efcc2db 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -54,3 +54,12 @@ func GetCurrentProject() string { } 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 +} diff --git a/utils.go b/utils.go deleted file mode 100644 index df7d04818..000000000 --- a/utils.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/fatih/color" -) - -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 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) -} From 9e725ae24e8fcecefec35a65b50476d371653ffb Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 21:16:21 +1000 Subject: [PATCH 08/19] got this bad boy compiling again --- pkg/app/app.go | 22 ++++- pkg/commands/git_structs.go | 6 +- pkg/commands/os.go | 28 +++--- pkg/gui/branches_panel.go | 4 +- pkg/gui/commits_panel.go | 6 +- pkg/gui/files_panel.go | 68 ++++++++------- pkg/gui/gui.go | 84 +++++++++--------- pkg/gui/keybindings.go | 18 ++-- pkg/gui/merge_panel.go | 165 +++++++++++++++++------------------- pkg/gui/stash_panel.go | 49 +++++------ pkg/gui/status_panel.go | 17 ++-- pkg/gui/view_helpers.go | 37 ++++---- pkg/utils/utils.go | 4 +- 13 files changed, 267 insertions(+), 241 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index b6318b745..011375737 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,9 +2,9 @@ package app import ( "io" + "os" "github.com/Sirupsen/logrus" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui" @@ -18,7 +18,21 @@ type App struct { Log *logrus.Logger OSCommand *commands.OSCommand GitCommand *commands.GitCommand - Gui *gocui.Gui + Gui *gui.Gui +} + +func newLogger(config config.AppConfigurer) *logrus.Logger { + log := logrus.New() + if !config.GetDebug() { + log.Out = nil + 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 @@ -28,7 +42,7 @@ func NewApp(config config.AppConfigurer) (*App, error) { Config: config, } var err error - app.Log = logrus.New() + app.Log = newLogger(config) app.OSCommand, err = commands.NewOSCommand(app.Log) if err != nil { return nil, err @@ -37,7 +51,7 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return nil, err } - app.Gui, err = gui.NewGui(app.Log, app.GitCommand, config.GetVersion()) + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, config.GetVersion()) if err != nil { return nil, err } diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go index 2f7255be1..6b10b18bb 100644 --- a/pkg/commands/git_structs.go +++ b/pkg/commands/git_structs.go @@ -30,7 +30,7 @@ type StashEntry struct { // 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 + Start int + Middle int + End int } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 2313b5550..3be1e1288 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/Sirupsen/logrus" - "github.com/jesseduffield/gocui" gitconfig "github.com/tcnksm/go-gitconfig" ) @@ -105,28 +104,34 @@ func (c *OSCommand) GetOpenCommand() (string, string, error) { // VsCodeOpenFile opens the file in code, with the -r flag to open in the // current window -func (c *OSCommand) VsCodeOpenFile(g *gocui.Gui, filename string) (string, error) { - return c.RunCommand("code -r " + filename) +// 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(g *gocui.Gui, filename string) (string, error) { - return c.RunCommand("subl " + filename) +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(g *gocui.Gui, filename string) (string, error) { +func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) { cmdName, cmdTrail, err := c.GetOpenCommand() if err != nil { - return "", err + return nil, err } - return c.RunCommand(cmdName + " " + filename + cmdTrail) + _, 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(g *gocui.Gui, filename string) (string, error) { +func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) { editor, _ := gitconfig.Global("core.editor") if editor == "" { editor = os.Getenv("VISUAL") @@ -140,10 +145,9 @@ func (c *OSCommand) editFile(g *gocui.Gui, filename string) (string, error) { } } if editor == "" { - return "", ErrNoEditorDefined + return nil, ErrNoEditorDefined } - c.PrepareSubProcess(editor, filename) - return "", nil + return c.PrepareSubProcess(editor, filename) } // PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 53b8465cb..2820b7b9e 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -117,7 +117,7 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { return nil } -// refreshStatus is called at the end of this because that's when we can +// 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 { @@ -135,7 +135,7 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { fmt.Fprintln(v, branch.GetDisplayString()) } gui.resetOrigin(v) - return refreshStatus(g) + return gui.refreshStatus(g) }) return nil } diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 45134e44a..faff6ad32 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -34,7 +34,7 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { shaColor.Fprint(v, commit.Sha+" ") white.Fprintln(v, commit.Name) } - refreshStatus(g) + gui.refreshStatus(g) if g.CurrentView().Name() == "commits" { gui.handleCommitSelect(g, v) } @@ -105,7 +105,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { if err := gui.refreshCommits(g); err != nil { panic(err) } - refreshStatus(g) + gui.refreshStatus(g) return gui.handleCommitSelect(g, v) } @@ -138,7 +138,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { if err := gui.refreshCommits(g); err != nil { panic(err) } - return refreshStatus(g) + return gui.refreshStatus(g) }, nil) return nil } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 9ffcafa44..8c60fcbf4 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -8,6 +8,7 @@ import ( // "strings" "errors" + "os/exec" "strings" "github.com/fatih/color" @@ -171,7 +172,7 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { gui.renderfilesOptions(g, &file) var content string if file.HasMergeConflicts { - return refreshMergePanel(g) + return gui.refreshMergePanel(g) } content = gui.GitCommand.Diff(file) @@ -191,9 +192,9 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { return nil } -// HandleCommitEditorPress - handle when the user wants to commit changes via +// 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 { +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") } @@ -214,7 +215,7 @@ func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error { return nil } -func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error { +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 { @@ -222,26 +223,31 @@ func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gu } return nil } - if _, err := open(g, file.Name); err != 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.editFile) + 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.openFile) + 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.sublimeOpenFile) + 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.vsCodeOpenFile) + return gui.genericFileOpen(g, v, gui.OSCommand.VsCodeOpenFile) } func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { @@ -250,13 +256,13 @@ func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) refreshStateFiles() { // get files to stage - files := getGitStatusFiles() - gui.State.Files = mergeGitStatusFiles(gui.State.Files, files) - updateHasMergeConflictStatus() + files := gui.GitCommand.GetStatusFiles() + gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files) + gui.updateHasMergeConflictStatus() } func (gui *Gui) updateHasMergeConflictStatus() error { - merging, err := isInMergeState() + merging, err := gui.GitCommand.IsInMergeState() if err != nil { return err } @@ -290,7 +296,7 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { } return "", gui.renderString(g, "main", "No file to display") } - cat, err := catFile(item.Name) + cat, err := gui.GitCommand.CatFile(item.Name) if err != nil { panic(err) } @@ -302,12 +308,12 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error { if err != nil { return err } - refreshStateFiles() + gui.refreshStateFiles() filesView.Clear() for _, file := range gui.State.Files { - renderFile(file, filesView) + gui.renderFile(file, filesView) } - correctCursor(filesView) + gui.correctCursor(filesView) if filesView == g.CurrentView() { gui.handleFileSelect(g, filesView) } @@ -315,14 +321,14 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error { } func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { - createMessagePanel(g, v, "", "Pulling...") + gui.createMessagePanel(g, v, "", "Pulling...") go func() { - if output, err := gitPull(); err != nil { + if output, err := gui.GitCommand.Pull(); err != nil { gui.createErrorPanel(g, output) } else { gui.closeConfirmationPrompt(g) - refreshCommits(g) - refreshStatus(g) + gui.refreshCommits(g) + gui.refreshStatus(g) } gui.refreshFiles(g) }() @@ -330,15 +336,15 @@ func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { - createMessagePanel(g, v, "", "Pushing...") + gui.createMessagePanel(g, v, "", "Pushing...") go func() { - branchName = gui.State.Branches[0].Name - if output, err := commands.Push(branchName); err != nil { + branchName := gui.State.Branches[0].Name + if output, err := gui.GitCommand.Push(branchName); err != nil { gui.createErrorPanel(g, output) } else { gui.closeConfirmationPrompt(g) - refreshCommits(g) - refreshStatus(g) + gui.refreshCommits(g) + gui.refreshStatus(g) } }() return nil @@ -360,22 +366,22 @@ func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(g, "This file has no merge conflicts") } gui.switchFocus(g, v, mergeView) - return refreshMergePanel(g) + return gui.refreshMergePanel(g) } func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error { - output, err := gitAbortMerge() + output, err := gui.GitCommand.AbortMerge() if err != nil { return gui.createErrorPanel(g, output) } - createMessagePanel(g, v, "", "Merge aborted") - refreshStatus(g) + 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 := commands.ResetHard(); err != nil { + if err := gui.GitCommand.ResetHard(); err != nil { gui.createErrorPanel(g, err.Error()) } return gui.refreshFiles(g) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 402b4ff32..aa524ce0d 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -37,12 +37,27 @@ type Gui struct { OSCommand *commands.OSCommand Version string SubProcess *exec.Cmd - State StateType + State guiState +} + +type guiState struct { + Files []commands.File + Branches []commands.Branch + Commits []commands.Commit + StashEntries []commands.StashEntry + PreviousView string + HasMergeConflicts bool + ConflictIndex int + ConflictTop bool + Conflicts []commands.Conflict + EditHistory *stack.Stack + Platform platform + Version string } // NewGui builds a new gui handler func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, version string) (*Gui, error) { - initialState := StateType{ + initialState := guiState{ Files: make([]commands.File, 0), PreviousView: "files", Commits: make([]commands.Commit, 0), @@ -64,21 +79,6 @@ func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *comm }, nil } -type StateType struct { - Files []commands.File - Branches []commands.Branch - Commits []commands.Commit - StashEntries []commands.StashEntry - PreviousView string - HasMergeConflicts bool - ConflictIndex int - ConflictTop bool - Conflicts []commands.Conflict - EditHistory *stack.Stack - Platform platform - Version string -} - type platform struct { os string shell string @@ -105,7 +105,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") ox, oy := mainView.Origin() if oy >= 1 { @@ -114,7 +114,7 @@ func scrollUpMain(g *gocui.Gui, v *gocui.View) error { 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") ox, oy := mainView.Origin() if oy < len(mainView.BufferLines()) { @@ -123,8 +123,8 @@ func scrollDownMain(g *gocui.Gui, v *gocui.View) error { return nil } -func handleRefresh(g *gocui.Gui, v *gocui.View) error { - return refreshSidePanels(g) +func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error { + return gui.refreshSidePanels(g) } func max(a, b int) int { @@ -257,35 +257,35 @@ func (gui *Gui) layout(g *gocui.Gui) error { // these are only called once gui.handleFileSelect(g, filesView) gui.refreshFiles(g) - refreshBranches(g) - refreshCommits(g) - refreshStashEntries(g) - nextView(g, nil) + gui.refreshBranches(g) + gui.refreshCommits(g) + gui.refreshStashEntries(g) + gui.nextView(g, nil) } - resizePopupPanels(g) + gui.resizePopupPanels(g) return nil } -func fetch(g *gocui.Gui) error { - gitFetch() - refreshStatus(g) +func (gui *Gui) fetch(g *gocui.Gui) error { + gui.GitCommand.Fetch() + gui.refreshStatus(g) return nil } -func updateLoader(g *gocui.Gui) error { +func (gui *Gui) updateLoader(g *gocui.Gui) error { if confirmationView, _ := g.View("confirmation"); confirmationView != nil { content := gui.trimmedContent(confirmationView) if strings.Contains(content, "...") { staticContent := strings.Split(content, "...")[0] + "..." - gui.renderString(g, "confirmation", staticContent+" "+loader()) + gui.renderString(g, "confirmation", staticContent+" "+gui.loader()) } } 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() { for range time.Tick(interval) { function(g) @@ -293,36 +293,36 @@ 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() if v.Name() == "commitMessage" || v.Name() == "confirmation" { - return resizePopupPanel(g, v) + return gui.resizePopupPanel(g, v) } return nil } // Run setup the gui with keybindings and start the mainloop -func (gui *Gui) Run() (*exec.Cmd, error) { +func (gui *Gui) Run() error { g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) if err != nil { - return nil, err + return err } defer g.Close() g.FgColor = gocui.ColorDefault - goEvery(g, time.Second*60, fetch) - goEvery(g, time.Second*10, gui.refreshFiles) - goEvery(g, time.Millisecond*10, updateLoader) + gui.goEvery(g, time.Second*60, gui.fetch) + gui.goEvery(g, time.Second*10, gui.refreshFiles) + gui.goEvery(g, time.Millisecond*10, gui.updateLoader) g.SetManagerFunc(gui.layout) if err = gui.keybindings(g); err != nil { - return nil, err + return err } err = g.MainLoop() - return nil, err + return err } // RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration @@ -342,6 +342,6 @@ func (gui *Gui) RunWithSubprocesses() { } } -func quit(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 194746d28..6a8a645f5 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -70,15 +70,15 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { // input in the confirmation panel for _, viewName := range []string{"files", "branches", "commits", "stash"} { bindings = append(bindings, []Binding{ - {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView}, - {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, - {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, - {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp}, - {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown}, - {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView}, - {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView}, - {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp}, - {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown}, + {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, + {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, + {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, + {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp}, + {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown}, + {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView}, + {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView}, + {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp}, + {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown}, }...) } diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 7e0a3a873..81e37f593 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -16,89 +16,89 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -func findConflicts(content string) ([]commands.Conflict, error) { +func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) { conflicts := make([]commands.Conflict, 0) - var newConflict conflict + var newConflict commands.Conflict for i, line := range utils.SplitLines(content) { if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" { - newConflict = conflict{start: i} + newConflict = commands.Conflict{Start: i} } else if line == "=======" { - newConflict.middle = i + newConflict.Middle = i } else if strings.HasPrefix(line, ">>>>>>> ") { - newConflict.end = i + newConflict.End = i conflicts = append(conflicts, newConflict) } } return conflicts, nil } -func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { +func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { return conflicts[0], conflicts[1:] } -func 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) 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 coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) { +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 := shiftConflict(conflicts) + conflict, remainingConflicts := gui.shiftConflict(conflicts) var outputBuffer bytes.Buffer - for i, line := range splitLines(content) { + for i, line := range utils.SplitLines(content) { colourAttr := color.FgWhite - if i == conflict.start || i == conflict.middle || i == conflict.end { + if i == conflict.Start || i == conflict.Middle || i == conflict.End { colourAttr = color.FgRed } colour := color.New(colourAttr) - if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) { + 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 = shiftConflict(remainingConflicts) + if i == conflict.End && len(remainingConflicts) > 0 { + conflict, remainingConflicts = gui.shiftConflict(remainingConflicts) } - outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n") + outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n") } return outputBuffer.String(), nil } -func handleSelectTop(g *gocui.Gui, v *gocui.View) error { - state.ConflictTop = true - return refreshMergePanel(g) +func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error { + gui.State.ConflictTop = true + return gui.refreshMergePanel(g) } -func handleSelectBottom(g *gocui.Gui, v *gocui.View) error { - state.ConflictTop = false - return refreshMergePanel(g) +func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error { + gui.State.ConflictTop = false + return gui.refreshMergePanel(g) } -func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error { - if state.ConflictIndex >= len(state.Conflicts)-1 { +func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error { + if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 { return nil } - state.ConflictIndex++ - return refreshMergePanel(g) + gui.State.ConflictIndex++ + return gui.refreshMergePanel(g) } -func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { - if state.ConflictIndex <= 0 { +func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { + if gui.State.ConflictIndex <= 0 { return nil } - state.ConflictIndex-- - return refreshMergePanel(g) + gui.State.ConflictIndex-- + return gui.refreshMergePanel(g) } -func isIndexToDelete(i int, conflict commands.Conflict, pick string) bool { - return i == conflict.middle || - i == conflict.start || - i == conflict.end || +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) + (pick == "bottom" && i > conflict.Start && i < conflict.Middle) || + (pick == "top" && i > conflict.Middle && i < conflict.End) } -func resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error { +func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error { gitFile, err := gui.getSelectedFile(g) if err != nil { return err @@ -116,126 +116,121 @@ func resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) erro if err != nil { break } - if !isIndexToDelete(i, conflict, pick) { + if !gui.isIndexToDelete(i, conflict, pick) { output += line } } - devLog(output) + gui.Log.Info(output) return ioutil.WriteFile(gitFile.Name, []byte(output), 0644) } -func pushFileSnapshot(g *gocui.Gui) error { +func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error { gitFile, err := gui.getSelectedFile(g) if err != nil { return err } - content, err := catFile(gitFile.Name) + content, err := gui.GitCommand.CatFile(gitFile.Name) if err != nil { return err } - state.EditHistory.Push(content) + gui.State.EditHistory.Push(content) return nil } -func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { - if state.EditHistory.Len() == 0 { +func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { + if gui.State.EditHistory.Len() == 0 { return nil } - prevContent := state.EditHistory.Pop().(string) + prevContent := gui.State.EditHistory.Pop().(string) gitFile, err := gui.getSelectedFile(g) if err != nil { return err } ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644) - return refreshMergePanel(g) + return gui.refreshMergePanel(g) } -func handlePickHunk(g *gocui.Gui, v *gocui.View) error { - conflict := state.Conflicts[state.ConflictIndex] - pushFileSnapshot(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 state.ConflictTop { + if gui.State.ConflictTop { pick = "top" } - err := resolveConflict(g, conflict, pick) + err := gui.resolveConflict(g, conflict, pick) if err != nil { panic(err) } - refreshMergePanel(g) + gui.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") +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 refreshMergePanel(g) + return gui.refreshMergePanel(g) } -func currentViewName(g *gocui.Gui) string { - currentView := g.CurrentView() - return currentView.Name() -} - -func refreshMergePanel(g *gocui.Gui) error { - cat, err := catSelectedFile(g) +func (gui *Gui) refreshMergePanel(g *gocui.Gui) error { + cat, err := gui.catSelectedFile(g) if err != nil { return err } - state.Conflicts, err = findConflicts(cat) + gui.State.Conflicts, err = gui.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 + 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 := currentViewName(g) == "main" + hasFocus := gui.currentViewName(g) == "main" if hasFocus { - renderMergeOptions(g) + gui.renderMergeOptions(g) } - content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus) + content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus) if err != nil { return err } - if err := scrollToConflict(g); err != nil { + if err := gui.scrollToConflict(g); err != nil { return err } return gui.renderString(g, "main", content) } -func scrollToConflict(g *gocui.Gui) error { +func (gui *Gui) scrollToConflict(g *gocui.Gui) error { mainView, err := g.View("main") if err != nil { return err } - if len(state.Conflicts) == 0 { + if len(gui.State.Conflicts) == 0 { return nil } - conflict := state.Conflicts[state.ConflictIndex] + conflict := gui.State.Conflicts[gui.State.ConflictIndex] ox, _ := mainView.Origin() _, height := mainView.Size() - conflictMiddle := (conflict.end + conflict.start) / 2 + 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 +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 refreshMergePanel(g) + return gui.refreshMergePanel(g) } -func renderMergeOptions(g *gocui.Gui) error { +func (gui *Gui) renderMergeOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ "↑ ↓": "select hunk", "← →": "navigate conflicts", @@ -245,7 +240,7 @@ func renderMergeOptions(g *gocui.Gui) error { }) } -func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { filesView, err := g.View("files") if err != nil { return err @@ -254,12 +249,12 @@ func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { return gui.switchFocus(g, v, filesView) } -func handleCompleteMerge(g *gocui.Gui) error { +func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error { filesView, err := g.View("files") if err != nil { return err } - stageSelectedFile(g) + gui.stageSelectedFile(g) gui.refreshFiles(g) return gui.switchFocus(g, nil, filesView) } diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 1af6bcc29..b1a209bb4 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -4,17 +4,18 @@ import ( "fmt" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" ) -func refreshStashEntries(g *gocui.Gui) error { +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) } - state.StashEntries = getGitStashEntries() + gui.State.StashEntries = gui.GitCommand.GetStashEntries() v.Clear() - for _, stashEntry := range state.StashEntries { + for _, stashEntry := range gui.State.StashEntries { fmt.Fprintln(v, stashEntry.DisplayString) } return gui.resetOrigin(v) @@ -22,15 +23,15 @@ func refreshStashEntries(g *gocui.Gui) error { return nil } -func getSelectedStashEntry(v *gocui.View) *StashEntry { - if len(state.StashEntries) == 0 { +func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry { + if len(gui.State.StashEntries) == 0 { return nil } lineNumber := gui.getItemPosition(v) - return &state.StashEntries[lineNumber] + return &gui.State.StashEntries[lineNumber] } -func renderStashOptions(g *gocui.Gui) error { +func (gui *Gui) renderStashOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ "space": "apply", "g": "pop", @@ -39,54 +40,54 @@ func renderStashOptions(g *gocui.Gui) error { }) } -func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { - if err := renderStashOptions(g); err != nil { +func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { + if err := gui.renderStashOptions(g); err != nil { return err } go func() { - stashEntry := getSelectedStashEntry(v) + stashEntry := gui.getSelectedStashEntry(v) if stashEntry == nil { gui.renderString(g, "main", "No stash entries") return } - diff, _ := getStashEntryDiff(stashEntry.Index) + diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index) gui.renderString(g, "main", diff) }() return nil } -func handleStashApply(g *gocui.Gui, v *gocui.View) error { - return stashDo(g, v, "apply") +func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error { + return gui.stashDo(g, v, "apply") } -func handleStashPop(g *gocui.Gui, v *gocui.View) error { - return stashDo(g, v, "pop") +func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error { + return gui.stashDo(g, v, "pop") } -func handleStashDrop(g *gocui.Gui, v *gocui.View) error { +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 stashDo(g, v, "drop") + return gui.stashDo(g, v, "drop") }, nil) } -func stashDo(g *gocui.Gui, v *gocui.View, method string) error { - stashEntry := getSelectedStashEntry(v) +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 := gitStashDo(stashEntry.Index, method); err != nil { + if output, err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil { gui.createErrorPanel(g, output) } - refreshStashEntries(g) + gui.refreshStashEntries(g) return gui.refreshFiles(g) } -func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { +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 := gitStashSave(gui.trimmedContent(v)); err != nil { + if output, err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil { gui.createErrorPanel(g, output) } - refreshStashEntries(g) + gui.refreshStashEntries(g) return gui.refreshFiles(g) }) return nil diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 822be2846..67f133738 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -5,9 +5,10 @@ import ( "github.com/fatih/color" "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") if err != nil { panic(err) @@ -17,22 +18,22 @@ func refreshStatus(g *gocui.Gui) error { // contents end up cleared g.Update(func(*gocui.Gui) error { v.Clear() - pushables, pullables := git.UpstreamDifferenceCount() + pushables, pullables := gui.GitCommand.UpstreamDifferenceCount() fmt.Fprint(v, "↑"+pushables+"↓"+pullables) - branches := state.Branches - if err := updateHasMergeConflictStatus(); err != nil { + branches := gui.State.Branches + if err := gui.updateHasMergeConflictStatus(); err != nil { return err } - if state.HasMergeConflicts { - fmt.Fprint(v, coloredString(" (merging)", color.FgYellow)) + if gui.State.HasMergeConflicts { + fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow)) } if len(branches) == 0 { return nil } branch := branches[0] - name := coloredString(branch.Name, branch.getColor()) - repo := getCurrentProject() + name := utils.ColoredString(branch.Name, branch.GetColor()) + repo := utils.GetCurrentRepoName() fmt.Fprint(v, " "+repo+" → "+name) return nil }) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a9564de5e..a3e2877c3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -13,7 +13,7 @@ var cyclableViews = []string{"files", "branches", "commits", "stash"} func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { gui.refreshBranches(g) - gui.gui.refreshFiles(g) + gui.refreshFiles(g) gui.refreshCommits(g) return nil } @@ -38,7 +38,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { if err != nil { panic(err) } - return gui.gui.switchFocus(g, v, focusedView) + return gui.switchFocus(g, v, focusedView) } func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { @@ -52,7 +52,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { break } 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 } } @@ -72,27 +72,27 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { case "files": return gui.handleFileSelect(g, v) case "branches": - return handleBranchSelect(g, v) + return gui.handleBranchSelect(g, v) case "confirmation": return nil case "commitMessage": - return handleCommitFocused(g, v) + return gui.handleCommitFocused(g, v) case "main": // TODO: pull this out into a 'view focused' function - refreshMergePanel(g) + gui.refreshMergePanel(g) v.Highlight = false return nil case "commits": - return handleCommitSelect(g, v) + return gui.handleCommitSelect(g, v) case "stash": - return handleStashEntrySelect(g, v) + return gui.handleStashEntrySelect(g, v) default: panic("No view matching newLineFocused switch statement") } } 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 { panic(err) } @@ -105,16 +105,16 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { // we should never stack confirmation panels if oldView != nil && oldView.Name() != "confirmation" { oldView.Highlight = false - devLog("setting previous view to:", oldView.Name()) - state.PreviousView = oldView.Name() + gui.Log.Info("setting previous view to:", oldView.Name()) + gui.State.PreviousView = oldView.Name() } 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 { return err } g.Cursor = newView.Editable - return newLineFocused(g, newView) + return gui.newLineFocused(g, newView) } func (gui *Gui) getItemPosition(v *gocui.View) int { @@ -138,7 +138,7 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { } } - newLineFocused(g, v) + gui.newLineFocused(g, v) return nil } @@ -159,7 +159,7 @@ func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { } } - newLineFocused(g, v) + gui.newLineFocused(g, v) return nil } @@ -207,7 +207,7 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { } func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { - return gui.renderString(g, "options", optionsMapToString(optionsMap)) + return gui.renderString(g, "options", gui.optionsMapToString(optionsMap)) } func (gui *Gui) loader() string { @@ -237,3 +237,8 @@ func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View { 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() +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b3efcc2db..68438246a 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -46,8 +46,8 @@ func ColoredStringDirect(str string, colour *color.Color) string { return colour.SprintFunc()(fmt.Sprint(str)) } -// GetCurrentProject gets the repo's base name -func GetCurrentProject() string { +// GetCurrentRepoName gets the repo's base name +func GetCurrentRepoName() string { pwd, err := os.Getwd() if err != nil { log.Fatalln(err.Error()) From 12de0345e4741456720724ceeb335b9804bf960b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 21:35:54 +1000 Subject: [PATCH 09/19] minor cleanup --- main.go | 54 -------------------------------------------------- pkg/gui/gui.go | 1 + 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/main.go b/main.go index fda30b6fb..c7fec1ae1 100644 --- a/main.go +++ b/main.go @@ -9,11 +9,8 @@ import ( "os/user" "path/filepath" - "github.com/davecgh/go-spew/spew" - "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/config" - git "gopkg.in/src-d/go-git.v4" ) var ( @@ -23,9 +20,6 @@ var ( debuggingFlag = flag.Bool("debug", false, "a boolean") versionFlag = flag.Bool("v", false, "Print the current version") - - w *git.Worktree - r *git.Repository ) func homeDirectory() string { @@ -41,37 +35,6 @@ func projectPath(path string) string { 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)) - } -} - // 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, // we will populate the `version` with VERSION in the lazygit root directory @@ -84,19 +47,6 @@ func fallbackVersion() string { 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() { flag.Parse() if version == "unversioned" { @@ -115,10 +65,6 @@ func main() { } app, err := app.NewApp(appConfig) app.Log.Info(err) - app.GitCommand.SetupGit() - // TODO remove this once r, w not used - setupWorktree() - app.Gui.RunWithSubprocesses() } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index aa524ce0d..c607ba79e 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -335,6 +335,7 @@ func (gui *Gui) RunWithSubprocesses() { break } else if err == ErrSubProcess { gui.SubProcess.Run() + gui.SubProcess = nil } else { log.Panicln(err) } From 3bd0246e4d752fe8b00c4a6dc41463dfd0ee0dd3 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 21:42:31 +1000 Subject: [PATCH 10/19] add test repo contents to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f516cbbaa..c57298e54 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ notes/go.notes TODO.notes TODO.md test/testrepo/ -test/repos/*/repo \ No newline at end of file +test/repos/*/repo +test/repos/*/repo/* From fb0004481b445b30b8180ddb27fb61e3c541337a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 21:42:56 +1000 Subject: [PATCH 11/19] correct cursor before returning item position --- pkg/gui/view_helpers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a3e2877c3..331e27975 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -118,6 +118,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { } func (gui *Gui) getItemPosition(v *gocui.View) int { + gui.correctCursor(v) _, cy := v.Cursor() _, oy := v.Origin() return oy + cy From 59e5545f38e161332acd481f7f8084ff2c57b33e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 23:35:01 +1000 Subject: [PATCH 12/19] discard log output when not in debug mode --- pkg/app/app.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 011375737..d5a8f11c9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -2,6 +2,7 @@ package app import ( "io" + "io/ioutil" "os" "github.com/Sirupsen/logrus" @@ -24,7 +25,7 @@ type App struct { func newLogger(config config.AppConfigurer) *logrus.Logger { log := logrus.New() if !config.GetDebug() { - log.Out = nil + log.Out = ioutil.Discard return log } file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY, 0666) From 95d2b0266408c99bcb6d64461b7f260b1dd4d2c0 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 23:36:06 +1000 Subject: [PATCH 13/19] update gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c57298e54..3d59e16c5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,4 @@ extra/lgit.rb notes/go.notes TODO.notes TODO.md -test/testrepo/ -test/repos/*/repo -test/repos/*/repo/* +test/repos/repo From ebfed34145f2bc76764c4bdf1659a5b890803fcb Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 23:46:08 +1000 Subject: [PATCH 14/19] add PR #135 keybindings to this branch --- pkg/gui/keybindings.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 6a8a645f5..b4f2bdc57 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -18,6 +18,8 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, + {ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, + {ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles}, {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles}, {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, From f549ad0f374828728dfd206e00e56469edd56d5a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 14 Aug 2018 07:27:59 +1000 Subject: [PATCH 15/19] use git command with message in subprocess if using gpgsign --- pkg/commands/git.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 016a08fc6..628b5f665 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -252,13 +252,13 @@ func (c *GitCommand) AbortMerge() (string, error) { // Commit commit to git func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { - command := "git commit -m \"" + message + "\"" + command := "commit -m \"" + message + "\"" gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { - return c.OSCommand.PrepareSubProcess("git", "commit") + return c.OSCommand.PrepareSubProcess("git", command) } // TODO: make these runDirectCommand functions just return an error - _, err := c.OSCommand.RunDirectCommand(command) + _, err := c.OSCommand.RunDirectCommand("git " + command) return nil, err } From d4f4b46a1f3a0d5aa2f430be931874dedc715ee2 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 14 Aug 2018 08:33:27 +1000 Subject: [PATCH 16/19] check both local and global config for gpgsign --- pkg/commands/git.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 628b5f665..28c368be2 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -250,15 +250,27 @@ 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 := "commit -m \"" + message + "\"" - gpgsign, _ := gitconfig.Global("commit.gpgsign") - if gpgsign != "" { - return c.OSCommand.PrepareSubProcess("git", command) + 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("git " + command) + _, err := c.OSCommand.RunDirectCommand(command) return nil, err } From 047892962a8f4f2f5acfb5e16ec9a14415316308 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 14 Aug 2018 08:33:40 +1000 Subject: [PATCH 17/19] centralise subprocess code to gui.go --- pkg/commands/os.go | 4 ---- pkg/gui/gui.go | 12 +++++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 3be1e1288..14a3721ad 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -153,9 +153,5 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) { // 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...) - subprocess.Stdin = os.Stdin - subprocess.Stdout = os.Stdout - subprocess.Stderr = os.Stderr - return subprocess, nil } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index c607ba79e..9cd7d35e6 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -6,7 +6,9 @@ import ( // "io/ioutil" "errors" + "io/ioutil" "log" + "os" "os/exec" "runtime" "strings" @@ -31,7 +33,7 @@ var ( // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { - Gui *gocui.Gui + g *gocui.Gui Log *logrus.Logger GitCommand *commands.GitCommand OSCommand *commands.OSCommand @@ -309,6 +311,8 @@ func (gui *Gui) Run() error { } defer g.Close() + gui.g = g // TODO: always use gui.g rather than passing g around everywhere + g.FgColor = gocui.ColorDefault gui.goEvery(g, time.Second*60, gui.fetch) @@ -334,7 +338,13 @@ func (gui *Gui) RunWithSubprocesses() { 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) From 45f640941c2f7797a686e9f4a58df2d03fcf8af2 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 14 Aug 2018 08:34:31 +1000 Subject: [PATCH 18/19] update dependencies --- Gopkg.lock | 20 +- vendor/github.com/Sirupsen/logrus/LICENSE | 21 + vendor/github.com/Sirupsen/logrus/alt_exit.go | 64 ++ vendor/github.com/Sirupsen/logrus/doc.go | 26 + vendor/github.com/Sirupsen/logrus/entry.go | 300 ++++++ vendor/github.com/Sirupsen/logrus/exported.go | 201 ++++ .../github.com/Sirupsen/logrus/formatter.go | 51 + vendor/github.com/Sirupsen/logrus/hooks.go | 34 + .../Sirupsen/logrus/json_formatter.go | 89 ++ vendor/github.com/Sirupsen/logrus/logger.go | 337 +++++++ vendor/github.com/Sirupsen/logrus/logrus.go | 143 +++ .../Sirupsen/logrus/terminal_bsd.go | 10 + .../logrus/terminal_check_appengine.go | 11 + .../logrus/terminal_check_notappengine.go | 19 + .../Sirupsen/logrus/terminal_linux.go | 14 + .../Sirupsen/logrus/text_formatter.go | 195 ++++ vendor/github.com/Sirupsen/logrus/writer.go | 62 ++ vendor/github.com/davecgh/go-spew/LICENSE | 15 - .../github.com/davecgh/go-spew/spew/bypass.go | 152 --- .../davecgh/go-spew/spew/bypasssafe.go | 38 - .../github.com/davecgh/go-spew/spew/common.go | 341 ------- .../github.com/davecgh/go-spew/spew/config.go | 306 ------ vendor/github.com/davecgh/go-spew/spew/doc.go | 211 ---- .../github.com/davecgh/go-spew/spew/dump.go | 509 ---------- .../github.com/davecgh/go-spew/spew/format.go | 419 -------- .../github.com/davecgh/go-spew/spew/spew.go | 148 --- vendor/github.com/jesseduffield/gocui/gui.go | 3 + .../x/crypto/ssh/terminal/terminal.go | 951 ++++++++++++++++++ .../golang.org/x/crypto/ssh/terminal/util.go | 114 +++ .../x/crypto/ssh/terminal/util_bsd.go | 12 + .../x/crypto/ssh/terminal/util_linux.go | 10 + .../x/crypto/ssh/terminal/util_plan9.go | 58 ++ .../x/crypto/ssh/terminal/util_solaris.go | 124 +++ .../x/crypto/ssh/terminal/util_windows.go | 103 ++ 34 files changed, 2962 insertions(+), 2149 deletions(-) create mode 100644 vendor/github.com/Sirupsen/logrus/LICENSE create mode 100644 vendor/github.com/Sirupsen/logrus/alt_exit.go create mode 100644 vendor/github.com/Sirupsen/logrus/doc.go create mode 100644 vendor/github.com/Sirupsen/logrus/entry.go create mode 100644 vendor/github.com/Sirupsen/logrus/exported.go create mode 100644 vendor/github.com/Sirupsen/logrus/formatter.go create mode 100644 vendor/github.com/Sirupsen/logrus/hooks.go create mode 100644 vendor/github.com/Sirupsen/logrus/json_formatter.go create mode 100644 vendor/github.com/Sirupsen/logrus/logger.go create mode 100644 vendor/github.com/Sirupsen/logrus/logrus.go create mode 100644 vendor/github.com/Sirupsen/logrus/terminal_bsd.go create mode 100644 vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go create mode 100644 vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go create mode 100644 vendor/github.com/Sirupsen/logrus/terminal_linux.go create mode 100644 vendor/github.com/Sirupsen/logrus/text_formatter.go create mode 100644 vendor/github.com/Sirupsen/logrus/writer.go delete mode 100644 vendor/github.com/davecgh/go-spew/LICENSE delete mode 100644 vendor/github.com/davecgh/go-spew/spew/bypass.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/bypasssafe.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/common.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/config.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/doc.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/dump.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/format.go delete mode 100644 vendor/github.com/davecgh/go-spew/spew/spew.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_linux.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_windows.go diff --git a/Gopkg.lock b/Gopkg.lock index 2e64ed23e..2c900cd99 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,12 +2,12 @@ [[projects]] - digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" - name = "github.com/davecgh/go-spew" - packages = ["spew"] + digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02" + name = "github.com/Sirupsen/logrus" + packages = ["."] pruneopts = "NUT" - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" + revision = "3e01752db0189b9157070a0e1668a620f9a85da2" + version = "v1.0.6" [[projects]] digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981" @@ -50,11 +50,11 @@ [[projects]] branch = "master" - digest = "1:e9b2b07a20f19d886267876b72ba15f2cbdeeeadd18030a4ce174b864e97c39e" + digest = "1:c9a848b0484a72da2dae28957b4f67501fe27fa38bc73f4713e454353c0a4a60" name = "github.com/jesseduffield/gocui" packages = ["."] pruneopts = "NUT" - revision = "8cecad864fb0b099a5f55bf1c97fbc1daca103e0" + revision = "432b7f6215f81ef1aaa1b2d9b69887822923cf79" [[projects]] digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba" @@ -151,7 +151,7 @@ [[projects]] branch = "master" - digest = "1:c76f8b24a4d9b99b502fb7b61ad769125075cb570efff9b9b73e6c428629532d" + digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4" name = "golang.org/x/crypto" packages = [ "cast5", @@ -170,6 +170,7 @@ "ssh", "ssh/agent", "ssh/knownhosts", + "ssh/terminal", ] pruneopts = "NUT" revision = "de0752318171da717af4ce24d0a2e8626afaeb11" @@ -282,14 +283,13 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/davecgh/go-spew/spew", + "github.com/Sirupsen/logrus", "github.com/fatih/color", "github.com/golang-collections/collections/stack", "github.com/jesseduffield/gocui", "github.com/tcnksm/go-gitconfig", "gopkg.in/src-d/go-git.v4", "gopkg.in/src-d/go-git.v4/plumbing", - "gopkg.in/src-d/go-git.v4/plumbing/object", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/Sirupsen/logrus/LICENSE b/vendor/github.com/Sirupsen/logrus/LICENSE new file mode 100644 index 000000000..f090cb42f --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/LICENSE @@ -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. diff --git a/vendor/github.com/Sirupsen/logrus/alt_exit.go b/vendor/github.com/Sirupsen/logrus/alt_exit.go new file mode 100644 index 000000000..8af90637a --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/alt_exit.go @@ -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 . +// +// 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) +} diff --git a/vendor/github.com/Sirupsen/logrus/doc.go b/vendor/github.com/Sirupsen/logrus/doc.go new file mode 100644 index 000000000..da67aba06 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/doc.go @@ -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 diff --git a/vendor/github.com/Sirupsen/logrus/entry.go b/vendor/github.com/Sirupsen/logrus/entry.go new file mode 100644 index 000000000..473bd1a0d --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/entry.go @@ -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] +} diff --git a/vendor/github.com/Sirupsen/logrus/exported.go b/vendor/github.com/Sirupsen/logrus/exported.go new file mode 100644 index 000000000..eb612a6f3 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/exported.go @@ -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...) +} diff --git a/vendor/github.com/Sirupsen/logrus/formatter.go b/vendor/github.com/Sirupsen/logrus/formatter.go new file mode 100644 index 000000000..83c74947b --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/formatter.go @@ -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) + } +} diff --git a/vendor/github.com/Sirupsen/logrus/hooks.go b/vendor/github.com/Sirupsen/logrus/hooks.go new file mode 100644 index 000000000..3f151cdc3 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/hooks.go @@ -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 +} diff --git a/vendor/github.com/Sirupsen/logrus/json_formatter.go b/vendor/github.com/Sirupsen/logrus/json_formatter.go new file mode 100644 index 000000000..dab17610f --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/json_formatter.go @@ -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 +} diff --git a/vendor/github.com/Sirupsen/logrus/logger.go b/vendor/github.com/Sirupsen/logrus/logger.go new file mode 100644 index 000000000..342f7977d --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/logger.go @@ -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) +} diff --git a/vendor/github.com/Sirupsen/logrus/logrus.go b/vendor/github.com/Sirupsen/logrus/logrus.go new file mode 100644 index 000000000..dd3899974 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/logrus.go @@ -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{}) +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_bsd.go b/vendor/github.com/Sirupsen/logrus/terminal_bsd.go new file mode 100644 index 000000000..4880d13d2 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_bsd.go @@ -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 diff --git a/vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go b/vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go new file mode 100644 index 000000000..3de08e802 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_check_appengine.go @@ -0,0 +1,11 @@ +// +build appengine gopherjs + +package logrus + +import ( + "io" +) + +func checkIfTerminal(w io.Writer) bool { + return true +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go b/vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go new file mode 100644 index 000000000..067047a12 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_check_notappengine.go @@ -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 + } +} diff --git a/vendor/github.com/Sirupsen/logrus/terminal_linux.go b/vendor/github.com/Sirupsen/logrus/terminal_linux.go new file mode 100644 index 000000000..f29a0097c --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/terminal_linux.go @@ -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 diff --git a/vendor/github.com/Sirupsen/logrus/text_formatter.go b/vendor/github.com/Sirupsen/logrus/text_formatter.go new file mode 100644 index 000000000..3e5504030 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/text_formatter.go @@ -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)) + } +} diff --git a/vendor/github.com/Sirupsen/logrus/writer.go b/vendor/github.com/Sirupsen/logrus/writer.go new file mode 100644 index 000000000..7bdebedc6 --- /dev/null +++ b/vendor/github.com/Sirupsen/logrus/writer.go @@ -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() +} diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE deleted file mode 100644 index c83641619..000000000 --- a/vendor/github.com/davecgh/go-spew/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2012-2016 Dave Collins - -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. diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go deleted file mode 100644 index 8a4a6589a..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/bypass.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2015-2016 Dave Collins -// -// 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<>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 -} diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go deleted file mode 100644 index 1fe3cf3d5..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015-2016 Dave Collins -// -// 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 -} diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go deleted file mode 100644 index 7c519ff47..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/common.go +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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("") - maxNewlineBytes = []byte("\n") - maxShortBytes = []byte("") - circularBytes = []byte("") - circularShortBytes = []byte("") - invalidAngleBytes = []byte("") - 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)) -} diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go deleted file mode 100644 index 2e3d22f31..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/config.go +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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: " "} -} diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go deleted file mode 100644 index aacaac6f1..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/doc.go +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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) - }), - 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 <*>} - %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} - %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} - %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} - -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 diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go deleted file mode 100644 index df1d582a7..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/dump.go +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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...) -} diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go deleted file mode 100644 index c49875bac..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/format.go +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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) -} diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/davecgh/go-spew/spew/spew.go deleted file mode 100644 index 32c0e3388..000000000 --- a/vendor/github.com/davecgh/go-spew/spew/spew.go +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2013-2016 Dave Collins - * - * 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 -} diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 5f090327b..28a52d1cd 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -364,6 +364,9 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) { // MainLoop runs the main loop until an error is returned. A successful // finish should return ErrQuit. func (g *Gui) MainLoop() error { + if err := g.flush(); err != nil { + return err + } go func() { for { g.tbEvents <- termbox.PollEvent() diff --git a/vendor/golang.org/x/crypto/ssh/terminal/terminal.go b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go new file mode 100644 index 000000000..9a887598f --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/terminal.go @@ -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 + } + } +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util.go b/vendor/golang.org/x/crypto/ssh/terminal/util.go new file mode 100644 index 000000000..731c89a28 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util.go @@ -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)) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go b/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go new file mode 100644 index 000000000..cb23a5904 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go @@ -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 diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go new file mode 100644 index 000000000..5fadfe8a1 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_linux.go @@ -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 diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go b/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go new file mode 100644 index 000000000..799f049f0 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go @@ -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) +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go new file mode 100644 index 000000000..9e41b9f43 --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go @@ -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 +} diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go new file mode 100644 index 000000000..8618955df --- /dev/null +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_windows.go @@ -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) +} From 7ecbd7fbb349ea5c4916677f992508f5b4011149 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 14 Aug 2018 08:42:08 +1000 Subject: [PATCH 19/19] update pre-commit hook --- test/repos/pre_commit_hook.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/repos/pre_commit_hook.sh b/test/repos/pre_commit_hook.sh index 8857f4145..1c24bf19f 100755 --- a/test/repos/pre_commit_hook.sh +++ b/test/repos/pre_commit_hook.sh @@ -2,7 +2,7 @@ set -ex; rm -rf repo; mkdir repo; cd repo git init -cp ../pre-commit .git/hooks/pre-commit +cp ../extras/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit echo "file" > file