├── History.md ├── LICENSE ├── Makefile ├── Readme.md ├── aws ├── dynamo │ └── dynamo.go └── metrics │ └── metrics.go ├── ci.yml ├── clipboard └── clipboard.go ├── database └── pg │ ├── array │ ├── array.go │ └── array_test.go │ ├── object │ └── object.go │ └── set │ ├── set.go │ └── set_test.go ├── doc.go ├── env └── env.go ├── flag └── usage │ └── usage.go ├── git ├── git.go └── git_test.go ├── go.mod ├── go.sum ├── graphviz └── graphviz.go ├── http ├── request │ ├── error.go │ └── error_test.go └── response │ ├── doc.go │ ├── error.go │ ├── error_test.go │ ├── json.go │ ├── json_test.go │ ├── pretty.go │ ├── status.go │ ├── status_test.go │ ├── xml.go │ └── xml_test.go ├── net ├── net.go └── net_test.go ├── semaphore └── semaphore.go ├── stripe └── hooks │ ├── hooks.go │ └── types.go └── term ├── term.go └── term_test.go /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.8.7 / 2020-06-03 3 | =================== 4 | 5 | * fix git error handling 6 | 7 | v1.8.6 / 2018-09-24 8 | =================== 9 | 10 | * term: refactor Renderer() to be more robust 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | include github.com/tj/make/golang 3 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Go 3 | 4 | Go packages that don't really deserve their own repos. 5 | 6 | ## Badges 7 | 8 | [](https://godoc.org/github.com/tj/go) 9 |  10 |  11 | [](https://apex.sh/) 12 | 13 | --- 14 | 15 | > [tjholowaychuk.com](http://tjholowaychuk.com) · 16 | > GitHub [@tj](https://github.com/tj) · 17 | > Twitter [@tjholowaychuk](https://twitter.com/tjholowaychuk) 18 | -------------------------------------------------------------------------------- /aws/dynamo/dynamo.go: -------------------------------------------------------------------------------- 1 | // Package dynamo provides dynamodb utilities. 2 | package dynamo 3 | 4 | import ( 5 | "github.com/aws/aws-sdk-go/service/dynamodb" 6 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 7 | ) 8 | 9 | // Item is a map of attributes. 10 | type Item map[string]*dynamodb.AttributeValue 11 | 12 | // Marshal returns a new item from struct. 13 | func Marshal(value interface{}) (Item, error) { 14 | v, err := dynamodbattribute.MarshalMap(value) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return Item(v), nil 20 | } 21 | 22 | // MustMarshal returns a new item from struct. 23 | func MustMarshal(value interface{}) Item { 24 | v, err := dynamodbattribute.MarshalMap(value) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | return Item(v) 30 | } 31 | 32 | // Unmarshal the item. 33 | func Unmarshal(i Item, value interface{}) error { 34 | return dynamodbattribute.UnmarshalMap(i, value) 35 | } 36 | -------------------------------------------------------------------------------- /aws/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Package metrics provides a simple interface for publishing CloudWatch metrics. 2 | package metrics 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/cloudwatch" 10 | "github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface" 11 | ) 12 | 13 | // Unit type. 14 | type Unit string 15 | 16 | // Unit types. 17 | const ( 18 | None Unit = "None" 19 | Seconds = "Seconds" 20 | Microseconds = "Microseconds" 21 | Milliseconds = "Milliseconds" 22 | Bytes = "Bytes" 23 | Kilobytes = "Kilobytes" 24 | Megabytes = "Megabytes" 25 | Gigabytes = "Gigabytes" 26 | Terabytes = "Terabytes" 27 | Bits = "Bits" 28 | Kilobits = "Kilobits" 29 | Megabits = "Megabits" 30 | Gigabits = "Gigabits" 31 | Terabits = "Terabits" 32 | Percent = "Percent" 33 | Count = "Count" 34 | BytesSecond = "Bytes/Second" 35 | KilobytesSecond = "Kilobytes/Second" 36 | MegabytesSecond = "Megabytes/Second" 37 | GigabytesSecond = "Gigabytes/Second" 38 | TerabytesSecond = "Terabytes/Second" 39 | BitsSecond = "Bits/Second" 40 | KilobitsSecond = "Kilobits/Second" 41 | MegabitsSecond = "Megabits/Second" 42 | GigabitsSecond = "Gigabits/Second" 43 | TerabitsSecond = "Terabits/Second" 44 | CountSecond = "Count/Second" 45 | ) 46 | 47 | // String implementation. 48 | func (u Unit) String() string { 49 | return string(u) 50 | } 51 | 52 | // Metric is a single metric. 53 | type Metric struct { 54 | name string 55 | unit Unit 56 | value float64 57 | namespace string 58 | dimensions []*cloudwatch.Dimension 59 | timestamp time.Time 60 | } 61 | 62 | // Dimension adds a dimension. 63 | func (m *Metric) Dimension(name, value string) *Metric { 64 | m.dimensions = append(m.dimensions, &cloudwatch.Dimension{ 65 | Name: &name, 66 | Value: &value, 67 | }) 68 | 69 | return m 70 | } 71 | 72 | // Unit sets the unit. 73 | func (m *Metric) Unit(kind Unit) *Metric { 74 | m.unit = kind 75 | return m 76 | } 77 | 78 | // Metrics buffers metrics. 79 | type Metrics struct { 80 | client cloudwatchiface.CloudWatchAPI 81 | namespace string 82 | buffer []*Metric 83 | } 84 | 85 | // New metrics with default client. 86 | func New(namespace string) *Metrics { 87 | return NewWithClient(cloudwatch.New(session.New(aws.NewConfig())), namespace) 88 | } 89 | 90 | // NewWithClient with custom client. 91 | func NewWithClient(client cloudwatchiface.CloudWatchAPI, namespace string) *Metrics { 92 | return &Metrics{ 93 | client: client, 94 | namespace: namespace, 95 | } 96 | } 97 | 98 | // Put metric. 99 | func (m *Metrics) Put(name string, value float64) *Metric { 100 | metric := &Metric{ 101 | name: name, 102 | namespace: m.namespace, 103 | timestamp: time.Now(), 104 | unit: None, 105 | value: value, 106 | } 107 | 108 | m.buffer = append(m.buffer, metric) 109 | return metric 110 | } 111 | 112 | // Flush metrics. 113 | func (m *Metrics) Flush() error { 114 | _, err := m.client.PutMetricData(&cloudwatch.PutMetricDataInput{ 115 | Namespace: &m.namespace, 116 | MetricData: m.metrics(), 117 | }) 118 | 119 | return err 120 | } 121 | 122 | // metrics returns cloudwatch metrics. 123 | func (m *Metrics) metrics() (metrics []*cloudwatch.MetricDatum) { 124 | for _, metric := range m.buffer { 125 | metrics = append(metrics, &cloudwatch.MetricDatum{ 126 | Dimensions: metric.dimensions, 127 | MetricName: &metric.name, 128 | Timestamp: &metric.timestamp, 129 | Unit: aws.String(metric.unit.String()), 130 | Value: &metric.value, 131 | }) 132 | } 133 | 134 | return 135 | } 136 | -------------------------------------------------------------------------------- /ci.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - go get -t ./... 7 | build: 8 | commands: 9 | - go test -cover -v ./... 10 | -------------------------------------------------------------------------------- /clipboard/clipboard.go: -------------------------------------------------------------------------------- 1 | package clipboard 2 | 3 | import "github.com/atotto/clipboard" 4 | 5 | // Write to clipboard. 6 | func Write(s string) error { 7 | return clipboard.WriteAll(s) 8 | } 9 | 10 | // Read from clipboard. 11 | func Read() (string, error) { 12 | return clipboard.ReadAll() 13 | } 14 | -------------------------------------------------------------------------------- /database/pg/array/array.go: -------------------------------------------------------------------------------- 1 | // Package array implements a JSONB array. 2 | package array 3 | 4 | import ( 5 | "database/sql/driver" 6 | "encoding/json" 7 | ) 8 | 9 | // Array type. 10 | type Array []interface{} 11 | 12 | // New returns an empty array. 13 | func New() Array { 14 | return make(Array, 0) 15 | } 16 | 17 | // Scan implementation. 18 | func (v *Array) Scan(src interface{}) error { 19 | switch src.(type) { 20 | case []byte: 21 | if err := json.Unmarshal(src.([]byte), &v); err != nil { 22 | return err 23 | } 24 | return nil 25 | default: 26 | return nil 27 | } 28 | } 29 | 30 | // Value implementation. 31 | func (v Array) Value() (driver.Value, error) { 32 | if v.Empty() { 33 | return "[]", nil 34 | } 35 | 36 | b, err := json.Marshal(v) 37 | return string(b), err 38 | } 39 | 40 | // Empty checks if the set is empty. 41 | func (v Array) Empty() bool { 42 | return len(v) == 0 43 | } 44 | 45 | // Interface assertion. 46 | var _ driver.Value = (Array)(nil) 47 | -------------------------------------------------------------------------------- /database/pg/array/array_test.go: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestArray_MarshalJSON(t *testing.T) { 11 | s := Array{"bar", 123} 12 | 13 | b, err := json.Marshal(s) 14 | assert.NoError(t, err, "marshal") 15 | 16 | assert.Equal(t, `["bar",123]`, string(b)) 17 | } 18 | 19 | func TestArray_Empty(t *testing.T) { 20 | t.Run("when empty", func(t *testing.T) { 21 | s := Array{} 22 | assert.True(t, s.Empty()) 23 | }) 24 | 25 | t.Run("when populated", func(t *testing.T) { 26 | s := Array{"foo"} 27 | assert.False(t, s.Empty()) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /database/pg/object/object.go: -------------------------------------------------------------------------------- 1 | // Package object implements a JSONB object. 2 | package object 3 | 4 | import ( 5 | "database/sql/driver" 6 | "encoding/json" 7 | ) 8 | 9 | // Object type. 10 | type Object map[string]interface{} 11 | 12 | // Scan implements the Scanner interface. 13 | func (v *Object) Scan(src interface{}) error { 14 | switch src.(type) { 15 | case []byte: 16 | return json.Unmarshal(src.([]byte), v) 17 | default: 18 | return nil 19 | } 20 | } 21 | 22 | // Value implements the Valuer interface. 23 | func (v Object) Value() (driver.Value, error) { 24 | if v.Empty() { 25 | return "{}", nil 26 | } 27 | 28 | b, err := json.Marshal(v) 29 | return string(b), err 30 | } 31 | 32 | // Keys of the object. 33 | func (v Object) Keys() (keys []string) { 34 | for k := range v { 35 | keys = append(keys, k) 36 | } 37 | return keys 38 | } 39 | 40 | // Size returns the number of values. 41 | func (v *Object) Size() int { 42 | return len(*v) 43 | } 44 | 45 | // Empty checks if the json has values. 46 | func (v *Object) Empty() bool { 47 | return len(*v) == 0 48 | } 49 | 50 | var _ driver.Value = (Object)(nil) 51 | -------------------------------------------------------------------------------- /database/pg/set/set.go: -------------------------------------------------------------------------------- 1 | // Package set implements a JSONB set. 2 | // 3 | // The Go-land type is backed by a string slice, while the 4 | // Postgres JSONB value is backed by an object. This is purely 5 | // for cosmetic reasons, if you have very large sets you should 6 | // use a map implementation. 7 | package set 8 | 9 | import ( 10 | "database/sql/driver" 11 | "encoding/json" 12 | ) 13 | 14 | // Set type. 15 | type Set []string 16 | 17 | // New returns an empty set. 18 | func New() Set { 19 | return make(Set, 0) 20 | } 21 | 22 | // Scan implementation. 23 | func (v *Set) Scan(src interface{}) error { 24 | switch src.(type) { 25 | case []byte: 26 | var m map[string]bool 27 | 28 | if err := json.Unmarshal(src.([]byte), &m); err != nil { 29 | return err 30 | } 31 | 32 | for k := range m { 33 | *v = append(*v, k) 34 | } 35 | 36 | return nil 37 | default: 38 | return nil 39 | } 40 | } 41 | 42 | // Value implementation. 43 | func (v Set) Value() (driver.Value, error) { 44 | if v.Empty() { 45 | return "{}", nil 46 | } 47 | 48 | m := make(map[string]bool) 49 | 50 | for _, s := range v { 51 | m[s] = true 52 | } 53 | 54 | b, err := json.Marshal(m) 55 | return string(b), err 56 | } 57 | 58 | // Add value to the set. 59 | func (v *Set) Add(value string) { 60 | if !v.Has(value) { 61 | *v = append(*v, value) 62 | } 63 | } 64 | 65 | // Remove value from the set. 66 | func (v *Set) Remove(value string) { 67 | for i, s := range *v { 68 | if s == value { 69 | *v = append((*v)[:i], (*v)[i+1:]...) 70 | return 71 | } 72 | } 73 | } 74 | 75 | // Has returns true if the value is present. 76 | func (v Set) Has(value string) bool { 77 | for _, s := range v { 78 | if s == value { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | // Values returns the set values as a slice. 86 | func (v Set) Values() []string { 87 | return v 88 | } 89 | 90 | // Empty checks if the set is empty. 91 | func (v Set) Empty() bool { 92 | return len(v) == 0 93 | } 94 | 95 | // Interface assertion. 96 | var _ driver.Value = (Set)(nil) 97 | -------------------------------------------------------------------------------- /database/pg/set/set_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestSet_MarshalJSON(t *testing.T) { 11 | s := Set{"bar"} 12 | 13 | b, err := json.Marshal(s) 14 | assert.NoError(t, err, "marshal") 15 | 16 | assert.Equal(t, `["bar"]`, string(b)) 17 | } 18 | 19 | func TestSet_Empty(t *testing.T) { 20 | t.Run("when empty", func(t *testing.T) { 21 | s := Set{} 22 | assert.True(t, s.Empty()) 23 | }) 24 | 25 | t.Run("when populated", func(t *testing.T) { 26 | s := Set{"foo"} 27 | assert.False(t, s.Empty()) 28 | }) 29 | } 30 | 31 | func TestSet_Add(t *testing.T) { 32 | s := Set{} 33 | s.Add("foo") 34 | s.Add("bar") 35 | s.Add("bar") 36 | s.Add("bar") 37 | assert.Equal(t, Set{"foo", "bar"}, s) 38 | } 39 | 40 | func TestSet_Has(t *testing.T) { 41 | s := Set{"foo", "bar"} 42 | assert.True(t, s.Has("foo")) 43 | assert.True(t, s.Has("bar")) 44 | assert.False(t, s.Has("baz")) 45 | } 46 | 47 | func TestSet_Remove(t *testing.T) { 48 | s := Set{"foo", "bar", "baz"} 49 | 50 | s.Remove("bar") 51 | assert.Equal(t, Set{"foo", "baz"}, s) 52 | 53 | s.Remove("bar") 54 | assert.Equal(t, Set{"foo", "baz"}, s) 55 | 56 | s.Remove("foo") 57 | assert.Equal(t, Set{"baz"}, s) 58 | 59 | s.Remove("baz") 60 | s.Remove("something") 61 | s.Remove("") 62 | assert.Equal(t, Set{}, s) 63 | } 64 | 65 | func TestSet_Value(t *testing.T) { 66 | t.Run("when empty", func(t *testing.T) { 67 | s := Set{} 68 | v, err := s.Value() 69 | assert.NoError(t, err, "value") 70 | assert.Equal(t, `{}`, v) 71 | }) 72 | 73 | t.Run("when populated", func(t *testing.T) { 74 | s := Set{"foo"} 75 | v, err := s.Value() 76 | assert.NoError(t, err, "value") 77 | assert.Equal(t, `{"foo":true}`, v) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package golang provides small Go utilities and Gopherjs packages. 2 | package golang 3 | -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | // Package env provides environment variable utilities. 2 | package env 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // Get panics if the environment variable is missing. 10 | func Get(name string) string { 11 | if s := os.Getenv(name); s == "" { 12 | panic(fmt.Errorf("environment variable %q is required", name)) 13 | } else { 14 | return s 15 | } 16 | } 17 | 18 | // GetDefault returns `value` if environment variable `name` is not present. 19 | func GetDefault(name string, value string) string { 20 | if s := os.Getenv(name); s == "" { 21 | return value 22 | } else { 23 | return s 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /flag/usage/usage.go: -------------------------------------------------------------------------------- 1 | // Package usage provides (subjectively) nicer flag package formatting. 2 | package usage 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | ) 10 | 11 | // Example of usage. 12 | type Example struct { 13 | // Help description. 14 | Help string 15 | 16 | // Command example. 17 | Command string 18 | } 19 | 20 | // Config for output. 21 | type Config struct { 22 | // Usage line (defaults to [options]). 23 | Usage string 24 | 25 | // Examples to output. 26 | Examples []Example 27 | } 28 | 29 | // Output usage. 30 | func Output(config *Config) func() { 31 | return func() { 32 | 33 | fmt.Fprintf(os.Stderr, "\n") 34 | fmt.Fprintf(os.Stderr, " Usage:\n\n") 35 | fmt.Fprintf(os.Stderr, " %s [options]\n\n", os.Args[0]) 36 | fmt.Fprintf(os.Stderr, " Flags:\n\n") 37 | 38 | flag.CommandLine.VisitAll(func(f *flag.Flag) { 39 | u := fmt.Sprintf(" -%s", f.Name) 40 | name, usage := flag.UnquoteUsage(f) 41 | 42 | if len(name) > 0 { 43 | u += " " + name 44 | } 45 | 46 | s := fmt.Sprintf("%-25s %s", u, usage) 47 | if !isZeroValue(f, f.DefValue) { 48 | // if _, ok := f.Value.(*stringValue); ok { 49 | // // put quotes on the value 50 | // s += fmt.Sprintf(" (default %q)", f.DefValue) 51 | // } else { 52 | s += fmt.Sprintf(" (default %v)", f.DefValue) 53 | // } 54 | } 55 | fmt.Fprint(os.Stderr, s, "\n") 56 | }) 57 | 58 | if len(config.Examples) > 0 { 59 | fmt.Fprintf(os.Stderr, "\n Examples:\n") 60 | for _, example := range config.Examples { 61 | fmt.Fprintf(os.Stderr, "\n %s\n", example.Help) 62 | fmt.Fprintf(os.Stderr, " $ %s\n", example.Command) 63 | } 64 | } 65 | 66 | fmt.Fprint(os.Stderr, "\n") 67 | } 68 | } 69 | 70 | // isZeroValue guesses whether the string represents the zero 71 | // value for a flag. It is not accurate but in practice works OK. 72 | func isZeroValue(f *flag.Flag, value string) bool { 73 | // Build a zero value of the flag's Value type, and see if the 74 | // result of calling its String method equals the value passed in. 75 | // This works unless the Value type is itself an interface type. 76 | typ := reflect.TypeOf(f.Value) 77 | var z reflect.Value 78 | if typ.Kind() == reflect.Ptr { 79 | z = reflect.New(typ.Elem()) 80 | } else { 81 | z = reflect.Zero(typ) 82 | } 83 | if value == z.Interface().(flag.Value).String() { 84 | return true 85 | } 86 | 87 | switch value { 88 | case "false": 89 | return true 90 | case "": 91 | return true 92 | case "0": 93 | return true 94 | } 95 | return false 96 | } 97 | -------------------------------------------------------------------------------- /git/git.go: -------------------------------------------------------------------------------- 1 | // Package git provides a few light-weight GIT utilities. 2 | package git 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Errors. 14 | var ( 15 | ErrDirty = errors.New("git repo is dirty") 16 | ErrNoRepo = errors.New("git repo not found") 17 | ErrLookup = errors.New("git is not installed") 18 | ) 19 | 20 | // GetRoot returns the git root relative to dir, if present. 21 | func GetRoot(dir string) (string, error) { 22 | cmd := exec.Command("git", "rev-parse", "--show-toplevel") 23 | cmd.Dir = dir 24 | b, err := output(cmd) 25 | return string(b), err 26 | } 27 | 28 | // Commit is commit meta-data. 29 | type Commit struct { 30 | AbbreviatedCommit string `json:"abbreviated_commit"` 31 | AbbreviatedParent string `json:"abbreviated_parent"` 32 | AbbreviatedTree string `json:"abbreviated_tree"` 33 | Author struct { 34 | Date string `json:"date"` 35 | Email string `json:"email"` 36 | Name string `json:"name"` 37 | } `json:"author"` 38 | Commit string `json:"commit"` 39 | Commiter struct { 40 | Date string `json:"date"` 41 | Email string `json:"email"` 42 | Name string `json:"name"` 43 | } `json:"commiter"` 44 | Parent string `json:"parent"` 45 | Refs string `json:"refs"` 46 | SanitizedSubjectLine string `json:"sanitized_subject_line"` 47 | Tree string `json:"tree"` 48 | } 49 | 50 | // Tag returns the tag or empty string. 51 | func (c *Commit) Tag() string { 52 | parts := strings.Split(c.Refs, ", ") 53 | for _, p := range parts { 54 | if strings.HasPrefix(p, "tag: ") { 55 | return strings.Replace(p, "tag: ", "", 1) 56 | } 57 | } 58 | return "" 59 | } 60 | 61 | // Describe returns the tag or sha. 62 | func (c *Commit) Describe() string { 63 | if t := c.Tag(); t != "" { 64 | return t 65 | } 66 | 67 | return c.AbbreviatedCommit 68 | } 69 | 70 | // GetCommit returns meta-data for the given commit within a repo. 71 | func GetCommit(dir, commit string) (c *Commit, err error) { 72 | dir, err = GetRoot(dir) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | cmd := exec.Command("git", "log", "-1", `--pretty=format:{"commit":"%H","abbreviated_commit":"%h","tree":"%T","abbreviated_tree":"%t","parent":"%P","abbreviated_parent":"%p","refs":"%D","author":{"name":"%aN","email":"%aE","date":"%aD"},"commiter":{"name":"%cN","email":"%cE","date":"%cD"}}`, commit) 78 | cmd.Dir = dir 79 | 80 | b, err := output(cmd) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if err := json.Unmarshal(b, &c); err != nil { 86 | return nil, errors.Wrap(err, "unmarshaling") 87 | } 88 | 89 | return c, nil 90 | } 91 | 92 | // output returns GIT command output with error normalization. 93 | func output(cmd *exec.Cmd) ([]byte, error) { 94 | out, err := cmd.CombinedOutput() 95 | 96 | if e, ok := err.(*exec.Error); ok { 97 | if e.Err == exec.ErrNotFound { 98 | return nil, ErrLookup 99 | } 100 | 101 | return nil, e 102 | } 103 | 104 | out = bytes.ToLower(out) 105 | 106 | switch { 107 | case bytes.Contains(out, []byte("not a git repository")): 108 | return nil, ErrNoRepo 109 | case bytes.Contains(out, []byte("ambiguous argument 'head'")): 110 | return nil, ErrNoRepo 111 | case bytes.Contains(out, []byte("dirty")): 112 | return nil, ErrDirty 113 | case err != nil: 114 | return nil, errors.New(string(out)) 115 | default: 116 | return bytes.TrimSpace(out), nil 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /git/git_test.go: -------------------------------------------------------------------------------- 1 | package git_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/tj/go/git" 9 | ) 10 | 11 | func TestGetRoot(t *testing.T) { 12 | t.Run("with a non-git dir", func(t *testing.T) { 13 | dir, err := git.GetRoot("/tmp") 14 | assert.EqualError(t, err, `git repo not found`) 15 | assert.Equal(t, "", dir) 16 | }) 17 | 18 | t.Run("with the root dir", func(t *testing.T) { 19 | dir, err := git.GetRoot("..") 20 | assert.NoError(t, err) 21 | assert.NotEmpty(t, dir) 22 | }) 23 | 24 | t.Run("with the root dir", func(t *testing.T) { 25 | a, err := git.GetRoot("..") 26 | assert.NoError(t, err) 27 | assert.NotEmpty(t, a) 28 | 29 | b, err := git.GetRoot(".") 30 | assert.NoError(t, err) 31 | assert.NotEmpty(t, b) 32 | 33 | assert.Equal(t, a, b) 34 | }) 35 | } 36 | 37 | func TestGetCommit(t *testing.T) { 38 | t.Run("direct HEAD", func(t *testing.T) { 39 | c, err := git.GetCommit("..", "HEAD") 40 | assert.NoError(t, err) 41 | assert.Len(t, c.Commit, 40) 42 | assert.NotEmpty(t, c.Author.Name) 43 | assert.NotEmpty(t, c.Author.Email) 44 | assert.NotEmpty(t, c.Author.Date) 45 | }) 46 | 47 | t.Run("relative HEAD", func(t *testing.T) { 48 | c, err := git.GetCommit(".", "HEAD") 49 | assert.NoError(t, err) 50 | assert.Len(t, c.Commit, 40) 51 | assert.NotEmpty(t, c.Author.Name) 52 | assert.NotEmpty(t, c.Author.Email) 53 | assert.NotEmpty(t, c.Author.Date) 54 | }) 55 | 56 | t.Run("relative sha", func(t *testing.T) { 57 | a, err := git.GetCommit(".", "HEAD") 58 | assert.NoError(t, err) 59 | assert.Len(t, a.Commit, 40) 60 | 61 | b, err := git.GetCommit(".", "642d730") 62 | assert.NoError(t, err) 63 | assert.Len(t, a.Commit, 40) 64 | 65 | assert.NotEqual(t, a.Commit, b.Commit, "commits") 66 | }) 67 | } 68 | 69 | func TestCommit_Tag(t *testing.T) { 70 | skipCI(t) 71 | 72 | t.Run("when a tag is present", func(t *testing.T) { 73 | c, err := git.GetCommit("..", "v1.7.0") 74 | assert.NoError(t, err) 75 | assert.Equal(t, `v1.7.0`, c.Tag()) 76 | }) 77 | 78 | t.Run("when a tag is not present", func(t *testing.T) { 79 | c, err := git.GetCommit("..", "9cd44c4") 80 | assert.NoError(t, err) 81 | assert.Equal(t, ``, c.Tag()) 82 | }) 83 | } 84 | 85 | func TestCommit_Describe(t *testing.T) { 86 | skipCI(t) 87 | 88 | t.Run("when a tag is present should use the tag", func(t *testing.T) { 89 | c, err := git.GetCommit("..", "v1.7.0") 90 | assert.NoError(t, err) 91 | assert.Equal(t, `v1.7.0`, c.Tag()) 92 | }) 93 | 94 | t.Run("when a tag is not present should use the sha", func(t *testing.T) { 95 | c, err := git.GetCommit("..", "9cd44c4") 96 | assert.NoError(t, err) 97 | assert.Equal(t, `9cd44c4`, c.Describe()) 98 | }) 99 | } 100 | 101 | // skipCI skips when in CI. 102 | func skipCI(t *testing.T) { 103 | if os.Getenv("CI") != "" { 104 | t.SkipNow() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tj/go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/apex/log v1.3.0 7 | github.com/atotto/clipboard v0.1.2 8 | github.com/aws/aws-sdk-go v1.31.9 9 | github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 10 | github.com/mattn/go-isatty v0.0.9 11 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 12 | github.com/pkg/errors v0.9.1 13 | github.com/stripe/stripe-go v70.15.0+incompatible 14 | github.com/tj/assert v0.0.1 15 | gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA= 2 | github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs= 3 | github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 4 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 5 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 6 | github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= 7 | github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 8 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 9 | github.com/aws/aws-sdk-go v1.31.9 h1:n+b34ydVfgC30j0Qm69yaapmjejQPW2BoDBX7Uy/tLI= 10 | github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 11 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 12 | github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 h1:uxxtrnACqI9zK4ENDMf0WpXfUsHP5V8liuq5QdgDISU= 13 | github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= 14 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 21 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 22 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 26 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 27 | github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= 28 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 29 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 30 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 31 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 32 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 36 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 37 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 38 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 39 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 40 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 41 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 42 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 43 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 44 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 45 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= 46 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 47 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 49 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 53 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 54 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 55 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 56 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 60 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 61 | github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= 62 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM= 64 | github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= 65 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 66 | github.com/tj/assert v0.0.1 h1:T7ozLNagrCCKl3wc+a706ztUCn/D6WHCJtkyvqYG+kQ= 67 | github.com/tj/assert v0.0.1/go.mod h1:lsg+GHQ0XplTcWKGxFLf/XPcPxWO8x2ut5jminoR2rA= 68 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 69 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 70 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 71 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 72 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 73 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 74 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 76 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 77 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 78 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 81 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 82 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 84 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 87 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 91 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 92 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 94 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 95 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 98 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 99 | gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa h1:5lGs+2OAqZvyIo1XjvoyXoDb8g6k9uAg2WTflQT/yl8= 100 | gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | -------------------------------------------------------------------------------- /graphviz/graphviz.go: -------------------------------------------------------------------------------- 1 | // Package graphviz provides some dot(1) utilities. 2 | package graphviz 3 | 4 | import ( 5 | "bytes" 6 | "io" 7 | "io/ioutil" 8 | "os/exec" 9 | 10 | "github.com/pkg/browser" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // lazy. 15 | var path = "/tmp/graph.html" 16 | 17 | // html template. 18 | var html = ` 19 |
20 | 21 |