From cfe3605e6b29e23db8dca9eedecc58cb13341587 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 11 Feb 2019 21:30:27 +1100 Subject: [PATCH 1/4] use go-errors package to display stacktrace of errors that cause panics --- pkg/commands/exec_live_default.go | 2 +- pkg/commands/git.go | 7 +++--- pkg/commands/os.go | 18 ++++++++------- pkg/commands/pull_request.go | 3 ++- pkg/git/patch_modifier.go | 3 ++- pkg/gui/commits_panel.go | 3 ++- pkg/gui/gui.go | 7 ++++-- pkg/gui/options_menu_panel.go | 2 +- pkg/gui/staging_panel.go | 2 +- pkg/test/test.go | 2 +- pkg/updates/updates.go | 3 ++- pkg/utils/utils.go | 3 ++- pkg/utils/utils_test.go | 37 ++++++++++++++++++------------- 13 files changed, 55 insertions(+), 37 deletions(-) diff --git a/pkg/commands/exec_live_default.go b/pkg/commands/exec_live_default.go index dfdff5308..2d0b78c21 100644 --- a/pkg/commands/exec_live_default.go +++ b/pkg/commands/exec_live_default.go @@ -5,7 +5,7 @@ package commands import ( "bufio" "bytes" - "errors" + "github.com/go-errors/errors" "os" "os/exec" "strings" diff --git a/pkg/commands/git.go b/pkg/commands/git.go index a1f01e422..4d9283a23 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,12 +1,13 @@ package commands import ( - "errors" "fmt" "os" "os/exec" "strings" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" @@ -27,11 +28,11 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f } if !os.IsNotExist(err) { - return err + return errors.Wrap(err, 0) } if err = chdir(".."); err != nil { - return err + return errors.Wrap(err, 0) } } } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index ffc0516ad..0bd9bf34a 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -1,13 +1,14 @@ package commands import ( - "errors" "io/ioutil" "os" "os/exec" "regexp" "strings" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/mgutz/str" @@ -122,7 +123,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { // errors like 'exit status 1' are not very useful so we'll create an error // from the combined output if outputString == "" { - return "", err + return "", errors.Wrap(err, 0) } return outputString, errors.New(outputString) } @@ -201,12 +202,12 @@ func (c *OSCommand) Unquote(message string) string { func (c *OSCommand) AppendLineToFile(filename, line string) error { f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { - return err + return errors.Wrap(err, 0) } defer f.Close() _, err = f.WriteString("\n" + line) - return err + return errors.Wrap(err, 0) } // CreateTempFile writes a string to a new temp file and returns the file's name @@ -214,16 +215,16 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { tmpfile, err := ioutil.TempFile("", filename) if err != nil { c.Log.Error(err) - return "", err + return "", errors.Wrap(err, 0) } if _, err := tmpfile.WriteString(content); err != nil { c.Log.Error(err) - return "", err + return "", errors.Wrap(err, 0) } if err := tmpfile.Close(); err != nil { c.Log.Error(err) - return "", err + return "", errors.Wrap(err, 0) } return tmpfile.Name(), nil @@ -231,5 +232,6 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { // RemoveFile removes a file at the specified path func (c *OSCommand) RemoveFile(filename string) error { - return os.Remove(filename) + err := os.Remove(filename) + return errors.Wrap(err, 0) } diff --git a/pkg/commands/pull_request.go b/pkg/commands/pull_request.go index 043a3c07d..7f0a55334 100644 --- a/pkg/commands/pull_request.go +++ b/pkg/commands/pull_request.go @@ -1,9 +1,10 @@ package commands import ( - "errors" "fmt" "strings" + + "github.com/go-errors/errors" ) // Service is a service that repository is on (Github, Bitbucket, ...) diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go index 3c523232e..f27040775 100644 --- a/pkg/git/patch_modifier.go +++ b/pkg/git/patch_modifier.go @@ -1,11 +1,12 @@ package git import ( - "errors" "regexp" "strconv" "strings" + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 4ff79960d..8ae1175be 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -1,9 +1,10 @@ package gui import ( - "errors" "fmt" + "github.com/go-errors/errors" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/utils" diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index ed7cfba0f..59cfd3a8b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -6,7 +6,6 @@ import ( // "io" // "io/ioutil" - "errors" "io/ioutil" "log" "os" @@ -14,6 +13,8 @@ import ( "strings" "time" + "github.com/go-errors/errors" + // "strings" "github.com/fatih/color" @@ -568,7 +569,9 @@ func (gui *Gui) RunWithSubprocesses() { gui.SubProcess.Stdin = nil gui.SubProcess = nil } else { - log.Panicln(err) + newErr := errors.Wrap(err, 0) + stackTrace := newErr.ErrorStack() + log.Panicln(stackTrace) } } } diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index ac01ad03d..cc736a5ae 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -1,7 +1,7 @@ package gui import ( - "errors" + "github.com/go-errors/errors" "github.com/jesseduffield/gocui" ) diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index 1408cfb45..836c978f2 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -1,7 +1,7 @@ package gui import ( - "errors" + "github.com/go-errors/errors" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/git" diff --git a/pkg/test/test.go b/pkg/test/test.go index 6346ac556..b3db699d0 100644 --- a/pkg/test/test.go +++ b/pkg/test/test.go @@ -1,7 +1,7 @@ package test import ( - "errors" + "github.com/go-errors/errors" "os" "os/exec" "path/filepath" diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index a08a2edcc..a4ec67e27 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -2,7 +2,6 @@ package updates import ( "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -13,6 +12,8 @@ import ( "strings" "time" + "github.com/go-errors/errors" + "github.com/kardianos/osext" getter "github.com/jesseduffield/go-getter" diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 390f85f70..60985dd0b 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,7 +2,6 @@ package utils import ( "encoding/json" - "errors" "fmt" "log" "os" @@ -11,6 +10,8 @@ import ( "strings" "time" + "github.com/go-errors/errors" + "github.com/fatih/color" ) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index f7545f5e9..ee502c1f8 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -1,7 +1,6 @@ package utils import ( - "errors" "testing" "github.com/stretchr/testify/assert" @@ -233,9 +232,9 @@ func TestGetDisplayStringArrays(t *testing.T) { // TestRenderDisplayableList is a function. func TestRenderDisplayableList(t *testing.T) { type scenario struct { - input []Displayable - expectedString string - expectedError error + input []Displayable + expectedString string + expectedErrorMessage string } scenarios := []scenario{ @@ -245,7 +244,7 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{}}), }, "\n", - nil, + "", }, { []Displayable{ @@ -253,7 +252,7 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{"c", "d"}}), }, "aa b\nc d", - nil, + "", }, { []Displayable{ @@ -261,23 +260,27 @@ func TestRenderDisplayableList(t *testing.T) { Displayable(&myDisplayable{[]string{"b", "c"}}), }, "", - errors.New("Each item must return the same number of strings to display"), + "Each item must return the same number of strings to display", }, } for _, s := range scenarios { str, err := renderDisplayableList(s.input) assert.EqualValues(t, s.expectedString, str) - assert.EqualValues(t, s.expectedError, err) + if s.expectedErrorMessage != "" { + assert.EqualError(t, err, s.expectedErrorMessage) + } else { + assert.NoError(t, err) + } } } // TestRenderList is a function. func TestRenderList(t *testing.T) { type scenario struct { - input interface{} - expectedString string - expectedError error + input interface{} + expectedString string + expectedErrorMessage string } scenarios := []scenario{ @@ -287,7 +290,7 @@ func TestRenderList(t *testing.T) { {[]string{"c", "d"}}, }, "aa b\nc d", - nil, + "", }, { []*myStruct{ @@ -295,19 +298,23 @@ func TestRenderList(t *testing.T) { {}, }, "", - errors.New("item does not implement the Displayable interface"), + "item does not implement the Displayable interface", }, { &myStruct{}, "", - errors.New("RenderList given a non-slice type"), + "RenderList given a non-slice type", }, } for _, s := range scenarios { str, err := RenderList(s.input) assert.EqualValues(t, s.expectedString, str) - assert.EqualValues(t, s.expectedError, err) + if s.expectedErrorMessage != "" { + assert.EqualError(t, err, s.expectedErrorMessage) + } else { + assert.NoError(t, err) + } } } From 0891797bf821c2324c7910218f006c0623b16122 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 11 Feb 2019 21:59:21 +1100 Subject: [PATCH 2/4] bump dep to include go-errors package --- Gopkg.lock | 9 + .../github.com/go-errors/errors/LICENSE.MIT | 7 + vendor/github.com/go-errors/errors/error.go | 217 ++++++++++++++++++ .../go-errors/errors/parse_panic.go | 127 ++++++++++ .../github.com/go-errors/errors/stackframe.go | 102 ++++++++ 5 files changed, 462 insertions(+) create mode 100644 vendor/github.com/go-errors/errors/LICENSE.MIT create mode 100644 vendor/github.com/go-errors/errors/error.go create mode 100644 vendor/github.com/go-errors/errors/parse_panic.go create mode 100644 vendor/github.com/go-errors/errors/stackframe.go diff --git a/Gopkg.lock b/Gopkg.lock index 432138f18..9fc55e20f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -96,6 +96,14 @@ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" +[[projects]] + digest = "1:ea1d5bfdb4ec5c2ee48c97865e6de1a28fa8c4849a3f56b27d521aa619038e06" + name = "github.com/go-errors/errors" + packages = ["."] + pruneopts = "NUT" + revision = "a6af135bd4e28680facf08a3d206b454abc877a4" + version = "v1.0.1" + [[projects]] digest = "1:74d9b0a7b4107b41e0ade759fac64502876f82d29fb23d77b3dd24b194ee3dd5" name = "github.com/go-ini/ini" @@ -620,6 +628,7 @@ input-imports = [ "github.com/cloudfoundry/jibber_jabber", "github.com/fatih/color", + "github.com/go-errors/errors", "github.com/golang-collections/collections/stack", "github.com/heroku/rollrus", "github.com/jesseduffield/go-getter", diff --git a/vendor/github.com/go-errors/errors/LICENSE.MIT b/vendor/github.com/go-errors/errors/LICENSE.MIT new file mode 100644 index 000000000..c9a5b2eeb --- /dev/null +++ b/vendor/github.com/go-errors/errors/LICENSE.MIT @@ -0,0 +1,7 @@ +Copyright (c) 2015 Conrad Irwin + +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/go-errors/errors/error.go b/vendor/github.com/go-errors/errors/error.go new file mode 100644 index 000000000..60062a437 --- /dev/null +++ b/vendor/github.com/go-errors/errors/error.go @@ -0,0 +1,217 @@ +// Package errors provides errors that have stack-traces. +// +// This is particularly useful when you want to understand the +// state of execution when an error was returned unexpectedly. +// +// It provides the type *Error which implements the standard +// golang error interface, so you can use this library interchangably +// with code that is expecting a normal error return. +// +// For example: +// +// package crashy +// +// import "github.com/go-errors/errors" +// +// var Crashed = errors.Errorf("oh dear") +// +// func Crash() error { +// return errors.New(Crashed) +// } +// +// This can be called as follows: +// +// package main +// +// import ( +// "crashy" +// "fmt" +// "github.com/go-errors/errors" +// ) +// +// func main() { +// err := crashy.Crash() +// if err != nil { +// if errors.Is(err, crashy.Crashed) { +// fmt.Println(err.(*errors.Error).ErrorStack()) +// } else { +// panic(err) +// } +// } +// } +// +// This package was original written to allow reporting to Bugsnag, +// but after I found similar packages by Facebook and Dropbox, it +// was moved to one canonical location so everyone can benefit. +package errors + +import ( + "bytes" + "fmt" + "reflect" + "runtime" +) + +// The maximum number of stackframes on any error. +var MaxStackDepth = 50 + +// Error is an error with an attached stacktrace. It can be used +// wherever the builtin error interface is expected. +type Error struct { + Err error + stack []uintptr + frames []StackFrame + prefix string +} + +// New makes an Error from the given value. If that value is already an +// error then it will be used directly, if not, it will be passed to +// fmt.Errorf("%v"). The stacktrace will point to the line of code that +// called New. +func New(e interface{}) *Error { + var err error + + switch e := e.(type) { + case error: + err = e + default: + err = fmt.Errorf("%v", e) + } + + stack := make([]uintptr, MaxStackDepth) + length := runtime.Callers(2, stack[:]) + return &Error{ + Err: err, + stack: stack[:length], + } +} + +// Wrap makes an Error from the given value. If that value is already an +// error then it will be used directly, if not, it will be passed to +// fmt.Errorf("%v"). The skip parameter indicates how far up the stack +// to start the stacktrace. 0 is from the current call, 1 from its caller, etc. +func Wrap(e interface{}, skip int) *Error { + var err error + + switch e := e.(type) { + case *Error: + return e + case error: + err = e + default: + err = fmt.Errorf("%v", e) + } + + stack := make([]uintptr, MaxStackDepth) + length := runtime.Callers(2+skip, stack[:]) + return &Error{ + Err: err, + stack: stack[:length], + } +} + +// WrapPrefix makes an Error from the given value. If that value is already an +// error then it will be used directly, if not, it will be passed to +// fmt.Errorf("%v"). The prefix parameter is used to add a prefix to the +// error message when calling Error(). The skip parameter indicates how far +// up the stack to start the stacktrace. 0 is from the current call, +// 1 from its caller, etc. +func WrapPrefix(e interface{}, prefix string, skip int) *Error { + + err := Wrap(e, 1+skip) + + if err.prefix != "" { + prefix = fmt.Sprintf("%s: %s", prefix, err.prefix) + } + + return &Error{ + Err: err.Err, + stack: err.stack, + prefix: prefix, + } + +} + +// Is detects whether the error is equal to a given error. Errors +// are considered equal by this function if they are the same object, +// or if they both contain the same error inside an errors.Error. +func Is(e error, original error) bool { + + if e == original { + return true + } + + if e, ok := e.(*Error); ok { + return Is(e.Err, original) + } + + if original, ok := original.(*Error); ok { + return Is(e, original.Err) + } + + return false +} + +// Errorf creates a new error with the given message. You can use it +// as a drop-in replacement for fmt.Errorf() to provide descriptive +// errors in return values. +func Errorf(format string, a ...interface{}) *Error { + return Wrap(fmt.Errorf(format, a...), 1) +} + +// Error returns the underlying error's message. +func (err *Error) Error() string { + + msg := err.Err.Error() + if err.prefix != "" { + msg = fmt.Sprintf("%s: %s", err.prefix, msg) + } + + return msg +} + +// Stack returns the callstack formatted the same way that go does +// in runtime/debug.Stack() +func (err *Error) Stack() []byte { + buf := bytes.Buffer{} + + for _, frame := range err.StackFrames() { + buf.WriteString(frame.String()) + } + + return buf.Bytes() +} + +// Callers satisfies the bugsnag ErrorWithCallerS() interface +// so that the stack can be read out. +func (err *Error) Callers() []uintptr { + return err.stack +} + +// ErrorStack returns a string that contains both the +// error message and the callstack. +func (err *Error) ErrorStack() string { + return err.TypeName() + " " + err.Error() + "\n" + string(err.Stack()) +} + +// StackFrames returns an array of frames containing information about the +// stack. +func (err *Error) StackFrames() []StackFrame { + if err.frames == nil { + err.frames = make([]StackFrame, len(err.stack)) + + for i, pc := range err.stack { + err.frames[i] = NewStackFrame(pc) + } + } + + return err.frames +} + +// TypeName returns the type this error. e.g. *errors.stringError. +func (err *Error) TypeName() string { + if _, ok := err.Err.(uncaughtPanic); ok { + return "panic" + } + return reflect.TypeOf(err.Err).String() +} diff --git a/vendor/github.com/go-errors/errors/parse_panic.go b/vendor/github.com/go-errors/errors/parse_panic.go new file mode 100644 index 000000000..cc37052d7 --- /dev/null +++ b/vendor/github.com/go-errors/errors/parse_panic.go @@ -0,0 +1,127 @@ +package errors + +import ( + "strconv" + "strings" +) + +type uncaughtPanic struct{ message string } + +func (p uncaughtPanic) Error() string { + return p.message +} + +// ParsePanic allows you to get an error object from the output of a go program +// that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap. +func ParsePanic(text string) (*Error, error) { + lines := strings.Split(text, "\n") + + state := "start" + + var message string + var stack []StackFrame + + for i := 0; i < len(lines); i++ { + line := lines[i] + + if state == "start" { + if strings.HasPrefix(line, "panic: ") { + message = strings.TrimPrefix(line, "panic: ") + state = "seek" + } else { + return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line) + } + + } else if state == "seek" { + if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") { + state = "parsing" + } + + } else if state == "parsing" { + if line == "" { + state = "done" + break + } + createdBy := false + if strings.HasPrefix(line, "created by ") { + line = strings.TrimPrefix(line, "created by ") + createdBy = true + } + + i++ + + if i >= len(lines) { + return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line) + } + + frame, err := parsePanicFrame(line, lines[i], createdBy) + if err != nil { + return nil, err + } + + stack = append(stack, *frame) + if createdBy { + state = "done" + break + } + } + } + + if state == "done" || state == "parsing" { + return &Error{Err: uncaughtPanic{message}, frames: stack}, nil + } + return nil, Errorf("could not parse panic: %v", text) +} + +// The lines we're passing look like this: +// +// main.(*foo).destruct(0xc208067e98) +// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 +func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { + idx := strings.LastIndex(name, "(") + if idx == -1 && !createdBy { + return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name) + } + if idx != -1 { + name = name[:idx] + } + pkg := "" + + if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { + pkg += name[:lastslash] + "/" + name = name[lastslash+1:] + } + if period := strings.Index(name, "."); period >= 0 { + pkg += name[:period] + name = name[period+1:] + } + + name = strings.Replace(name, "·", ".", -1) + + if !strings.HasPrefix(line, "\t") { + return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line) + } + + idx = strings.LastIndex(line, ":") + if idx == -1 { + return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line) + } + file := line[1:idx] + + number := line[idx+1:] + if idx = strings.Index(number, " +"); idx > -1 { + number = number[:idx] + } + + lno, err := strconv.ParseInt(number, 10, 32) + if err != nil { + return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line) + } + + return &StackFrame{ + File: file, + LineNumber: int(lno), + Package: pkg, + Name: name, + }, nil +} diff --git a/vendor/github.com/go-errors/errors/stackframe.go b/vendor/github.com/go-errors/errors/stackframe.go new file mode 100644 index 000000000..750ab9a52 --- /dev/null +++ b/vendor/github.com/go-errors/errors/stackframe.go @@ -0,0 +1,102 @@ +package errors + +import ( + "bytes" + "fmt" + "io/ioutil" + "runtime" + "strings" +) + +// A StackFrame contains all necessary information about to generate a line +// in a callstack. +type StackFrame struct { + // The path to the file containing this ProgramCounter + File string + // The LineNumber in that file + LineNumber int + // The Name of the function that contains this ProgramCounter + Name string + // The Package that contains this function + Package string + // The underlying ProgramCounter + ProgramCounter uintptr +} + +// NewStackFrame popoulates a stack frame object from the program counter. +func NewStackFrame(pc uintptr) (frame StackFrame) { + + frame = StackFrame{ProgramCounter: pc} + if frame.Func() == nil { + return + } + frame.Package, frame.Name = packageAndName(frame.Func()) + + // pc -1 because the program counters we use are usually return addresses, + // and we want to show the line that corresponds to the function call + frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) + return + +} + +// Func returns the function that contained this frame. +func (frame *StackFrame) Func() *runtime.Func { + if frame.ProgramCounter == 0 { + return nil + } + return runtime.FuncForPC(frame.ProgramCounter) +} + +// String returns the stackframe formatted in the same way as go does +// in runtime/debug.Stack() +func (frame *StackFrame) String() string { + str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) + + source, err := frame.SourceLine() + if err != nil { + return str + } + + return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) +} + +// SourceLine gets the line of code (from File and Line) of the original source if possible. +func (frame *StackFrame) SourceLine() (string, error) { + data, err := ioutil.ReadFile(frame.File) + + if err != nil { + return "", New(err) + } + + lines := bytes.Split(data, []byte{'\n'}) + if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { + return "???", nil + } + // -1 because line-numbers are 1 based, but our array is 0 based + return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil +} + +func packageAndName(fn *runtime.Func) (string, string) { + name := fn.Name() + pkg := "" + + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Since the package path might contains dots (e.g. code.google.com/...), + // we first remove the path prefix if there is one. + if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { + pkg += name[:lastslash] + "/" + name = name[lastslash+1:] + } + if period := strings.Index(name, "."); period >= 0 { + pkg += name[:period] + name = name[period+1:] + } + + name = strings.Replace(name, "·", ".", -1) + return pkg, name +} From 53e73313a26bc757ccc8868098a567995948ee2a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 11 Feb 2019 22:00:54 +1100 Subject: [PATCH 3/4] bump gocui to version that uses go-errors as well --- Gopkg.lock | 4 ++-- vendor/github.com/jesseduffield/gocui/edit.go | 2 +- vendor/github.com/jesseduffield/gocui/escape.go | 2 +- vendor/github.com/jesseduffield/gocui/gui.go | 2 +- vendor/github.com/jesseduffield/gocui/view.go | 3 ++- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 9fc55e20f..bc65dc14d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -197,11 +197,11 @@ [[projects]] branch = "master" - digest = "1:9b266d7748a5d94985fd9e323494f5b8ae1ab3e910418e898dfe7f03339ddbcd" + digest = "1:a783e08c25b01d2b1b447469bc6e80a666ddac91087fb286864a81c34d9da99d" name = "github.com/jesseduffield/gocui" packages = ["."] pruneopts = "NUT" - revision = "cfa9e452ba5ebf014041846851152d64a59dce14" + revision = "985c2f8d59e55de7e78b1ecf72277041df7dd44d" [[projects]] branch = "master" diff --git a/vendor/github.com/jesseduffield/gocui/edit.go b/vendor/github.com/jesseduffield/gocui/edit.go index c339d75fb..b99f74f93 100644 --- a/vendor/github.com/jesseduffield/gocui/edit.go +++ b/vendor/github.com/jesseduffield/gocui/edit.go @@ -5,7 +5,7 @@ package gocui import ( - "errors" + "github.com/go-errors/errors" "github.com/mattn/go-runewidth" ) diff --git a/vendor/github.com/jesseduffield/gocui/escape.go b/vendor/github.com/jesseduffield/gocui/escape.go index ec31bbe0d..c88309b07 100644 --- a/vendor/github.com/jesseduffield/gocui/escape.go +++ b/vendor/github.com/jesseduffield/gocui/escape.go @@ -5,7 +5,7 @@ package gocui import ( - "errors" + "github.com/go-errors/errors" "strconv" ) diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index cb163e06d..a50d0c522 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -5,7 +5,7 @@ package gocui import ( - "errors" + "github.com/go-errors/errors" "github.com/jesseduffield/termbox-go" ) diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 73df1dbcf..f691960b4 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -6,10 +6,11 @@ package gocui import ( "bytes" - "errors" "io" "strings" + "github.com/go-errors/errors" + "github.com/jesseduffield/termbox-go" "github.com/mattn/go-runewidth" ) From e09f3905e962be254bffb9736f88799e61fa5fcf Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 11 Feb 2019 22:16:25 +1100 Subject: [PATCH 4/4] update go.mod --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c01fd368f..505827242 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/emirpasic/gods v1.9.0 github.com/fatih/color v1.7.0 github.com/fsnotify/fsnotify v1.4.7 + github.com/go-errors/errors v1.0.1 github.com/go-ini/ini v1.38.2 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 @@ -18,7 +19,7 @@ require ( github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 - github.com/jesseduffield/gocui v0.0.0-20190115084758-cfa9e452ba5e + github.com/jesseduffield/gocui v0.0.0-20190211105959-985c2f8d59e5 github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8