├── 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 | [![GoDoc](https://godoc.org/github.com/tj/go?status.svg)](https://godoc.org/github.com/tj/go) 9 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 10 | ![](https://img.shields.io/badge/status-stable-green.svg) 11 | [![](http://apex.sh/images/badge.svg)](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 |
{graph}
22 | 25 | 26 | ` 27 | 28 | // render template. 29 | func render(graph []byte) []byte { 30 | return bytes.Replace([]byte(html), []byte("{graph}"), graph, -1) 31 | } 32 | 33 | // OpenDot opens the given reader as zoomable SVG in the browser. 34 | func OpenDot(r io.Reader) error { 35 | cmd := exec.Command("dot", "-Tsvg") 36 | cmd.Stdin = r 37 | 38 | b, err := cmd.CombinedOutput() 39 | if err != nil { 40 | return errors.Wrap(err, "executing") 41 | } 42 | 43 | err = ioutil.WriteFile(path, render(b), 0755) 44 | if err != nil { 45 | return errors.Wrap(err, "writing") 46 | } 47 | 48 | return browser.OpenURL("file://" + path) 49 | } 50 | 51 | // Must helper. 52 | func Must(err error) { 53 | if err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /http/request/error.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "net/http" 4 | 5 | // Error is an HTTP error. 6 | type Error int 7 | 8 | // Error implementation. 9 | func (e Error) Error() string { 10 | return http.StatusText(int(e)) 11 | } 12 | 13 | // IsStatus returns true if err is status code. 14 | func IsStatus(err error, code int) bool { 15 | e, ok := err.(Error) 16 | return ok && int(e) == code 17 | } 18 | 19 | // IsClient returns true if err represents a 4xx error. 20 | func IsClient(err error) bool { 21 | e, ok := err.(Error) 22 | return ok && e >= 400 && e < 500 23 | } 24 | 25 | // IsServer returns true if err represents a 5xx error. 26 | func IsServer(err error) bool { 27 | e, ok := err.(Error) 28 | return ok && e >= 500 29 | } 30 | 31 | // IsNotFound returns true if err is a 404. 32 | func IsNotFound(err error) bool { 33 | return IsStatus(err, 404) 34 | } 35 | 36 | // Param returns the parameter by name. 37 | func Param(r *http.Request, name string) string { 38 | return r.URL.Query().Get(name) 39 | } 40 | 41 | // ParamDefault returns the parameter by name or default value. 42 | func ParamDefault(r *http.Request, name, value string) string { 43 | if s := Param(r, name); s != "" { 44 | return s 45 | } 46 | return value 47 | } 48 | -------------------------------------------------------------------------------- /http/request/error_test.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestError(t *testing.T) { 10 | assert.Equal(t, "Not Found", Error(404).Error()) 11 | assert.Equal(t, "Bad Request", Error(400).Error()) 12 | } 13 | 14 | func TestIsNotFound(t *testing.T) { 15 | assert.True(t, IsNotFound(Error(404))) 16 | assert.False(t, IsNotFound(Error(500))) 17 | } 18 | 19 | func TestIsStatus(t *testing.T) { 20 | assert.True(t, IsStatus(Error(404), 404)) 21 | assert.True(t, IsStatus(Error(500), 500)) 22 | assert.False(t, IsStatus(Error(500), 400)) 23 | } 24 | 25 | func TestIsClient(t *testing.T) { 26 | assert.True(t, IsClient(Error(404))) 27 | assert.False(t, IsClient(Error(500))) 28 | } 29 | 30 | func TestIsServer(t *testing.T) { 31 | assert.False(t, IsServer(Error(404))) 32 | assert.True(t, IsServer(Error(500))) 33 | } 34 | -------------------------------------------------------------------------------- /http/response/doc.go: -------------------------------------------------------------------------------- 1 | // Package response provides http response helpers. 2 | package response 3 | -------------------------------------------------------------------------------- /http/response/error.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "net/http" 4 | 5 | // Error responds with a generic status code response. 6 | func Error(w http.ResponseWriter, code int) { 7 | http.Error(w, http.StatusText(code), code) 8 | } 9 | -------------------------------------------------------------------------------- /http/response/error_test.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/tj/assert" 9 | ) 10 | 11 | func TestError(t *testing.T) { 12 | res := httptest.NewRecorder() 13 | Error(res, http.StatusBadRequest) 14 | assert.Equal(t, 400, res.Code) 15 | assert.Equal(t, "Bad Request\n", string(res.Body.Bytes())) 16 | assert.Equal(t, "text/plain; charset=utf-8", res.HeaderMap["Content-Type"][0]) 17 | } 18 | -------------------------------------------------------------------------------- /http/response/json.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "encoding/json" 4 | import "net/http" 5 | 6 | // JSON response with optional status code. 7 | func JSON(w http.ResponseWriter, val interface{}, code ...int) { 8 | var b []byte 9 | var err error 10 | 11 | if Pretty { 12 | b, err = json.MarshalIndent(val, "", " ") 13 | } else { 14 | b, err = json.Marshal(val) 15 | } 16 | 17 | if err != nil { 18 | http.Error(w, err.Error(), http.StatusInternalServerError) 19 | return 20 | } 21 | 22 | w.Header().Set("Content-Type", "application/json") 23 | 24 | if len(code) > 0 { 25 | w.WriteHeader(code[0]) 26 | } 27 | 28 | w.Write(b) 29 | } 30 | -------------------------------------------------------------------------------- /http/response/json_test.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | type User struct { 11 | First string `json:"first"` 12 | Last string `json:"last"` 13 | } 14 | 15 | func TestJSONPretty(t *testing.T) { 16 | Pretty = true 17 | res := httptest.NewRecorder() 18 | JSON(res, &User{"Tobi", "Ferret"}) 19 | assert.Equal(t, 200, res.Code) 20 | assert.Equal(t, "{\n \"first\": \"Tobi\",\n \"last\": \"Ferret\"\n}", string(res.Body.Bytes())) 21 | assert.Equal(t, "application/json", res.HeaderMap["Content-Type"][0]) 22 | } 23 | 24 | func TestJSON(t *testing.T) { 25 | Pretty = false 26 | res := httptest.NewRecorder() 27 | JSON(res, &User{"Tobi", "Ferret"}) 28 | assert.Equal(t, 200, res.Code) 29 | assert.Equal(t, `{"first":"Tobi","last":"Ferret"}`, string(res.Body.Bytes())) 30 | assert.Equal(t, "application/json", res.HeaderMap["Content-Type"][0]) 31 | } 32 | -------------------------------------------------------------------------------- /http/response/pretty.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | // Pretty response formatting. 4 | var Pretty = true 5 | -------------------------------------------------------------------------------- /http/response/status.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // respond with the given message. 9 | func text(w http.ResponseWriter, code int, msg string) { 10 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 11 | w.WriteHeader(code) 12 | fmt.Fprintln(w, msg) 13 | } 14 | 15 | // write `msg` to the response writer, currently only a single argument is supported. 16 | func write(w http.ResponseWriter, code int, msg []interface{}) { 17 | if len(msg) == 0 { 18 | text(w, code, http.StatusText(code)) 19 | return 20 | } 21 | 22 | switch msg[0].(type) { 23 | case string: 24 | text(w, code, msg[0].(string)) 25 | default: 26 | JSON(w, msg[0], code) 27 | } 28 | } 29 | 30 | // Continue response. 31 | func Continue(w http.ResponseWriter, msg ...interface{}) { 32 | write(w, http.StatusContinue, msg) 33 | } 34 | 35 | // SwitchingProtocols response. 36 | func SwitchingProtocols(w http.ResponseWriter, msg ...interface{}) { 37 | write(w, http.StatusSwitchingProtocols, msg) 38 | } 39 | 40 | // OK response. 41 | func OK(w http.ResponseWriter, msg ...interface{}) { 42 | write(w, http.StatusOK, msg) 43 | } 44 | 45 | // Created response. 46 | func Created(w http.ResponseWriter, msg ...interface{}) { 47 | write(w, http.StatusCreated, msg) 48 | } 49 | 50 | // Accepted response. 51 | func Accepted(w http.ResponseWriter, msg ...interface{}) { 52 | write(w, http.StatusAccepted, msg) 53 | } 54 | 55 | // NonAuthoritativeInfo response. 56 | func NonAuthoritativeInfo(w http.ResponseWriter, msg ...interface{}) { 57 | write(w, http.StatusNonAuthoritativeInfo, msg) 58 | } 59 | 60 | // NoContent response. 61 | func NoContent(w http.ResponseWriter, msg ...interface{}) { 62 | write(w, http.StatusNoContent, msg) 63 | } 64 | 65 | // ResetContent response. 66 | func ResetContent(w http.ResponseWriter, msg ...interface{}) { 67 | write(w, http.StatusResetContent, msg) 68 | } 69 | 70 | // PartialContent response. 71 | func PartialContent(w http.ResponseWriter, msg ...interface{}) { 72 | write(w, http.StatusPartialContent, msg) 73 | } 74 | 75 | // MultipleChoices response. 76 | func MultipleChoices(w http.ResponseWriter, msg ...interface{}) { 77 | write(w, http.StatusMultipleChoices, msg) 78 | } 79 | 80 | // MovedPermanently response. 81 | func MovedPermanently(w http.ResponseWriter, msg ...interface{}) { 82 | write(w, http.StatusMovedPermanently, msg) 83 | } 84 | 85 | // Found response. 86 | func Found(w http.ResponseWriter, msg ...interface{}) { 87 | write(w, http.StatusFound, msg) 88 | } 89 | 90 | // SeeOther response. 91 | func SeeOther(w http.ResponseWriter, msg ...interface{}) { 92 | write(w, http.StatusSeeOther, msg) 93 | } 94 | 95 | // NotModified response. 96 | func NotModified(w http.ResponseWriter, msg ...interface{}) { 97 | write(w, http.StatusNotModified, msg) 98 | } 99 | 100 | // UseProxy response. 101 | func UseProxy(w http.ResponseWriter, msg ...interface{}) { 102 | write(w, http.StatusUseProxy, msg) 103 | } 104 | 105 | // TemporaryRedirect response. 106 | func TemporaryRedirect(w http.ResponseWriter, msg ...interface{}) { 107 | write(w, http.StatusTemporaryRedirect, msg) 108 | } 109 | 110 | // BadRequest response. 111 | func BadRequest(w http.ResponseWriter, msg ...interface{}) { 112 | write(w, http.StatusBadRequest, msg) 113 | } 114 | 115 | // Unauthorized response. 116 | func Unauthorized(w http.ResponseWriter, msg ...interface{}) { 117 | write(w, http.StatusUnauthorized, msg) 118 | } 119 | 120 | // PaymentRequired response. 121 | func PaymentRequired(w http.ResponseWriter, msg ...interface{}) { 122 | write(w, http.StatusPaymentRequired, msg) 123 | } 124 | 125 | // Forbidden response. 126 | func Forbidden(w http.ResponseWriter, msg ...interface{}) { 127 | write(w, http.StatusForbidden, msg) 128 | } 129 | 130 | // NotFound response. 131 | func NotFound(w http.ResponseWriter, msg ...interface{}) { 132 | write(w, http.StatusNotFound, msg) 133 | } 134 | 135 | // MethodNotAllowed response. 136 | func MethodNotAllowed(w http.ResponseWriter, msg ...interface{}) { 137 | write(w, http.StatusMethodNotAllowed, msg) 138 | } 139 | 140 | // NotAcceptable response. 141 | func NotAcceptable(w http.ResponseWriter, msg ...interface{}) { 142 | write(w, http.StatusNotAcceptable, msg) 143 | } 144 | 145 | // ProxyAuthRequired response. 146 | func ProxyAuthRequired(w http.ResponseWriter, msg ...interface{}) { 147 | write(w, http.StatusProxyAuthRequired, msg) 148 | } 149 | 150 | // RequestTimeout response. 151 | func RequestTimeout(w http.ResponseWriter, msg ...interface{}) { 152 | write(w, http.StatusRequestTimeout, msg) 153 | } 154 | 155 | // Conflict response. 156 | func Conflict(w http.ResponseWriter, msg ...interface{}) { 157 | write(w, http.StatusConflict, msg) 158 | } 159 | 160 | // Gone response. 161 | func Gone(w http.ResponseWriter, msg ...interface{}) { 162 | write(w, http.StatusGone, msg) 163 | } 164 | 165 | // LengthRequired response. 166 | func LengthRequired(w http.ResponseWriter, msg ...interface{}) { 167 | write(w, http.StatusLengthRequired, msg) 168 | } 169 | 170 | // PreconditionFailed response. 171 | func PreconditionFailed(w http.ResponseWriter, msg ...interface{}) { 172 | write(w, http.StatusPreconditionFailed, msg) 173 | } 174 | 175 | // RequestEntityTooLarge response. 176 | func RequestEntityTooLarge(w http.ResponseWriter, msg ...interface{}) { 177 | write(w, http.StatusRequestEntityTooLarge, msg) 178 | } 179 | 180 | // RequestURITooLong response. 181 | func RequestURITooLong(w http.ResponseWriter, msg ...interface{}) { 182 | write(w, http.StatusRequestURITooLong, msg) 183 | } 184 | 185 | // UnsupportedMediaType response. 186 | func UnsupportedMediaType(w http.ResponseWriter, msg ...interface{}) { 187 | write(w, http.StatusUnsupportedMediaType, msg) 188 | } 189 | 190 | // RequestedRangeNotSatisfiable response. 191 | func RequestedRangeNotSatisfiable(w http.ResponseWriter, msg ...interface{}) { 192 | write(w, http.StatusRequestedRangeNotSatisfiable, msg) 193 | } 194 | 195 | // ExpectationFailed response. 196 | func ExpectationFailed(w http.ResponseWriter, msg ...interface{}) { 197 | write(w, http.StatusExpectationFailed, msg) 198 | } 199 | 200 | // Teapot response. 201 | func Teapot(w http.ResponseWriter, msg ...interface{}) { 202 | write(w, http.StatusTeapot, msg) 203 | } 204 | 205 | // InternalServerError response. 206 | func InternalServerError(w http.ResponseWriter, msg ...interface{}) { 207 | write(w, http.StatusInternalServerError, msg) 208 | } 209 | 210 | // NotImplemented response. 211 | func NotImplemented(w http.ResponseWriter, msg ...interface{}) { 212 | write(w, http.StatusNotImplemented, msg) 213 | } 214 | 215 | // BadGateway response. 216 | func BadGateway(w http.ResponseWriter, msg ...interface{}) { 217 | write(w, http.StatusBadGateway, msg) 218 | } 219 | 220 | // ServiceUnavailable response. 221 | func ServiceUnavailable(w http.ResponseWriter, msg ...interface{}) { 222 | write(w, http.StatusServiceUnavailable, msg) 223 | } 224 | 225 | // GatewayTimeout response. 226 | func GatewayTimeout(w http.ResponseWriter, msg ...interface{}) { 227 | write(w, http.StatusGatewayTimeout, msg) 228 | } 229 | 230 | // HTTPVersionNotSupported response. 231 | func HTTPVersionNotSupported(w http.ResponseWriter, msg ...interface{}) { 232 | write(w, http.StatusHTTPVersionNotSupported, msg) 233 | } 234 | -------------------------------------------------------------------------------- /http/response/status_test.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestStatusFunctions(t *testing.T) { 11 | res := httptest.NewRecorder() 12 | NotFound(res) 13 | assert.Equal(t, 404, res.Code) 14 | assert.Equal(t, "Not Found\n", string(res.Body.Bytes())) 15 | assert.Equal(t, "text/plain; charset=utf-8", res.HeaderMap["Content-Type"][0]) 16 | } 17 | 18 | func TestStatusFunctionsMessage(t *testing.T) { 19 | res := httptest.NewRecorder() 20 | NotFound(res, "can't find that") 21 | assert.Equal(t, 404, res.Code) 22 | assert.Equal(t, "can't find that\n", string(res.Body.Bytes())) 23 | assert.Equal(t, "text/plain; charset=utf-8", res.HeaderMap["Content-Type"][0]) 24 | } 25 | 26 | func TestStatusFunctionsJSON(t *testing.T) { 27 | res := httptest.NewRecorder() 28 | Unauthorized(res, map[string]string{"error": "token_expired", "message": "Token expired!"}) 29 | assert.Equal(t, 401, res.Code) 30 | assert.Equal(t, `{"error":"token_expired","message":"Token expired!"}`, string(res.Body.Bytes())) 31 | assert.Equal(t, "application/json", res.HeaderMap["Content-Type"][0]) 32 | } 33 | -------------------------------------------------------------------------------- /http/response/xml.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "encoding/xml" 4 | import "net/http" 5 | 6 | // XML response with optional status code. 7 | func XML(w http.ResponseWriter, val interface{}, code ...int) { 8 | var b []byte 9 | var err error 10 | 11 | if Pretty { 12 | b, err = xml.MarshalIndent(val, "", " ") 13 | } else { 14 | b, err = xml.Marshal(val) 15 | } 16 | 17 | if err != nil { 18 | http.Error(w, err.Error(), http.StatusInternalServerError) 19 | return 20 | } 21 | 22 | w.Header().Set("Content-Type", "application/xml") 23 | 24 | if len(code) > 0 { 25 | w.WriteHeader(code[0]) 26 | } 27 | 28 | w.Write(b) 29 | } 30 | -------------------------------------------------------------------------------- /http/response/xml_test.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestXMLPretty(t *testing.T) { 11 | Pretty = true 12 | res := httptest.NewRecorder() 13 | XML(res, &User{"Tobi", "Ferret"}) 14 | assert.Equal(t, 200, res.Code) 15 | assert.Equal(t, "\n Tobi\n Ferret\n", string(res.Body.Bytes())) 16 | assert.Equal(t, "application/xml", res.HeaderMap["Content-Type"][0]) 17 | } 18 | 19 | func TestXML(t *testing.T) { 20 | Pretty = false 21 | res := httptest.NewRecorder() 22 | XML(res, &User{"Tobi", "Ferret"}) 23 | assert.Equal(t, 200, res.Code) 24 | assert.Equal(t, `TobiFerret`, string(res.Body.Bytes())) 25 | assert.Equal(t, "application/xml", res.HeaderMap["Content-Type"][0]) 26 | } 27 | -------------------------------------------------------------------------------- /net/net.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "net" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // GetCert returns the certificate of the given url. 15 | func GetCert(uri string) (*x509.Certificate, error) { 16 | u, err := url.Parse(uri) 17 | if err != nil { 18 | return nil, errors.Wrap(err, "parsing url") 19 | } 20 | 21 | if u.Scheme != "https" { 22 | return nil, errors.New("https only") 23 | } 24 | 25 | if !strings.Contains(u.Host, ":") { 26 | u.Host += ":443" 27 | } 28 | 29 | d := &net.Dialer{ 30 | Timeout: 10 * time.Second, 31 | } 32 | 33 | conn, err := tls.DialWithDialer(d, "tcp", u.Host, nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | cert := conn.ConnectionState().PeerCertificates[0] 39 | return cert, nil 40 | } 41 | 42 | // Issuer information. 43 | type Issuer struct { 44 | Name string `json:"name"` 45 | Country string `json:"country"` 46 | Organization string `json:"organization"` 47 | } 48 | 49 | // Summary for the certificate. 50 | type Summary struct { 51 | IssuedAt time.Time `json:"issued_at"` 52 | ExpiresAt time.Time `json:"expires_at"` 53 | Issuer Issuer `json:"issuer"` 54 | Domains []string `json:"domains"` 55 | IssuingCertificateURL string `json:"issuing_certificate_url"` 56 | } 57 | 58 | // GetCertSummary returns a summary of the certificate. 59 | func GetCertSummary(url string) (*Summary, error) { 60 | c, err := GetCert(url) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return &Summary{ 66 | IssuedAt: c.NotBefore, 67 | ExpiresAt: c.NotAfter, 68 | Domains: c.DNSNames, 69 | IssuingCertificateURL: first(c.IssuingCertificateURL), 70 | Issuer: Issuer{ 71 | Name: c.Issuer.CommonName, 72 | Country: first(c.Issuer.Country), 73 | Organization: first(c.Issuer.Organization), 74 | }, 75 | }, nil 76 | } 77 | 78 | // first string in slice or an empty string. 79 | func first(s []string) string { 80 | if len(s) > 0 { 81 | return s[0] 82 | } 83 | 84 | return "" 85 | } 86 | -------------------------------------------------------------------------------- /net/net_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestGetCert(t *testing.T) { 10 | t.Run("valid https", func(t *testing.T) { 11 | c, err := GetCert("https://apex.sh") 12 | assert.NoError(t, err, "cert") 13 | assert.NotEmpty(t, c, "empty cert") 14 | }) 15 | 16 | t.Run("explicit port", func(t *testing.T) { 17 | c, err := GetCert("https://apex.sh:443") 18 | assert.NoError(t, err, "cert") 19 | assert.NotEmpty(t, c, "empty cert") 20 | }) 21 | 22 | t.Run("http", func(t *testing.T) { 23 | _, err := GetCert("http://apex.sh") 24 | assert.EqualError(t, err, `https only`) 25 | }) 26 | } 27 | 28 | func TestGetCertSummary(t *testing.T) { 29 | t.Run("valid https", func(t *testing.T) { 30 | c, err := GetCertSummary("https://apex.sh") 31 | assert.NoError(t, err, "cert") 32 | assert.NotEmpty(t, c.IssuedAt) 33 | assert.NotEmpty(t, c.ExpiresAt) 34 | assert.Equal(t, "Amazon", c.Issuer.Name) 35 | assert.Equal(t, "Amazon", c.Issuer.Organization) 36 | assert.Equal(t, "US", c.Issuer.Country) 37 | assert.Equal(t, []string{"apex.sh", "*.apex.sh"}, c.Domains) 38 | assert.True(t, c.IssuedAt.Before(c.ExpiresAt)) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /semaphore/semaphore.go: -------------------------------------------------------------------------------- 1 | // Package semaphore provides a simple channel-based semaphore. 2 | package semaphore 3 | 4 | // Semaphore channel. 5 | type Semaphore chan struct{} 6 | 7 | // New semaphore with the given `concurrency`. 8 | func New(concurrency int) Semaphore { 9 | return make(Semaphore, concurrency) 10 | } 11 | 12 | // Acquire resource. 13 | func (s Semaphore) Acquire() { 14 | s <- struct{}{} 15 | } 16 | 17 | // Release resource. 18 | func (s Semaphore) Release() { 19 | <-s 20 | } 21 | 22 | // Wait for completion. 23 | func (s Semaphore) Wait() { 24 | for i := 0; i < cap(s); i++ { 25 | s <- struct{}{} 26 | } 27 | } 28 | 29 | // Run `fn` in a goroutine, acquiring then releasing after its return. 30 | func (s Semaphore) Run(fn func()) { 31 | s.Acquire() 32 | go func() { 33 | defer s.Release() 34 | fn() 35 | }() 36 | } 37 | -------------------------------------------------------------------------------- /stripe/hooks/hooks.go: -------------------------------------------------------------------------------- 1 | // Package hooks provides Stripe webhook handling with secret support. 2 | package hooks 3 | 4 | import ( 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/apex/log" 9 | "github.com/stripe/stripe-go" 10 | "github.com/stripe/stripe-go/webhook" 11 | "github.com/tj/go/http/response" 12 | ) 13 | 14 | // Func is an event handler function. 15 | type Func func(*stripe.Event) error 16 | 17 | // New event handler with secret. 18 | func New(secret string, h Func) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | b, err := ioutil.ReadAll(r.Body) 21 | if err != nil { 22 | log.WithError(err).Error("reading body") 23 | response.InternalServerError(w) 24 | return 25 | } 26 | 27 | signature := r.Header.Get("Stripe-Signature") 28 | e, err := webhook.ConstructEvent(b, signature, secret) 29 | if err != nil { 30 | log.WithError(err).Error("constructing event") 31 | response.InternalServerError(w) 32 | return 33 | } 34 | 35 | ctx := log.WithFields(log.Fields{ 36 | "event_id": e.ID, 37 | "event_type": e.Type, 38 | }) 39 | 40 | ctx.Info("handling stripe event") 41 | if err := h(&e); err != nil { 42 | ctx.WithError(err).Error("handling stripe event") 43 | response.InternalServerError(w) 44 | return 45 | } 46 | 47 | ctx.Info("handled stripe event") 48 | response.OK(w) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /stripe/hooks/types.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | // EventType is the type of event for the hook. 4 | type EventType string 5 | 6 | // Event types available. 7 | const ( 8 | AccountUpdated EventType = "account.updated" // Occurs whenever an account status or property has changed. 9 | AccountApplicationDeauthorized = "account.application.deauthorized" // Occurs whenever a user deauthorizes an application. Sent to the related application only. 10 | AccountExternalAccountCreated = "account.external_account.created" // Occurs whenever an external account is created. 11 | AccountExternalAccountDeleted = "account.external_account.deleted" // Occurs whenever an external account is deleted. 12 | AccountExternalAccountUpdated = "account.external_account.updated" // Occurs whenever an external account is updated. 13 | ApplicationFeeCreated = "application_fee.created" // Occurs whenever an application fee is created on a charge. 14 | ApplicationFeeRefunded = "application_fee.refunded" // Occurs whenever an application fee is refunded, whether from refunding a charge or from refunding the application fee directly, including partial refunds. 15 | ApplicationFeeRefundUpdated = "application_fee.refund.updated" // Occurs whenever an application fee refund is updated. 16 | BalanceAvailable = "balance.available" // Occurs whenever your Stripe balance has been updated (e.g. when a charge collected is available to be paid out). By default, Stripe will automatically transfer any funds in your balance to your bank account on a daily basis. 17 | BitcoinReceiverCreated = "bitcoin.receiver.created" // Occurs whenever a receiver has been created. 18 | BitcoinReceiverFilled = "bitcoin.receiver.filled" // Occurs whenever a receiver is filled (that is, when it has received enough bitcoin to process a payment of the same amount). 19 | BitcoinReceiverUpdated = "bitcoin.receiver.updated" // Occurs whenever a receiver is updated. 20 | BitcoinReceiverTransactionCreated = "bitcoin.receiver.transaction.created" // Occurs whenever bitcoin is pushed to a receiver. 21 | ChargeCaptured = "charge.captured" // Occurs whenever a previously uncaptured charge is captured. 22 | ChargeFailed = "charge.failed" // Occurs whenever a failed charge attempt occurs. 23 | ChargeRefunded = "charge.refunded" // Occurs whenever a charge is refunded, including partial refunds. 24 | ChargeSucceeded = "charge.succeeded" // Occurs whenever a new charge is created and is successful. 25 | ChargeUpdated = "charge.updated" // Occurs whenever a charge description or metadata is updated. 26 | ChargeDisputeClosed = "charge.dispute.closed" // Occurs when the dispute is closed and the dispute status changes to charge_refunded, lost, warning_closed, or won. 27 | ChargeDisputeCreated = "charge.dispute.created" // Occurs whenever a customer disputes a charge with their bank (chargeback). 28 | ChargeDisputeFundsReinstated = "charge.dispute.funds_reinstated" // Occurs when funds are reinstated to your account after a dispute is won. 29 | ChargeDisputeFundsWithdrawn = "charge.dispute.funds_withdrawn" // Occurs when funds are removed from your account due to a dispute. 30 | ChargeDisputeUpdated = "charge.dispute.updated" // Occurs when the dispute is updated (usually with evidence). 31 | CouponCreated = "coupon.created" // Occurs whenever a coupon is created. 32 | CouponDeleted = "coupon.deleted" // Occurs whenever a coupon is deleted. 33 | CouponUpdated = "coupon.updated" // Occurs whenever a coupon is updated. 34 | CustomerCreated = "customer.created" // Occurs whenever a new customer is created. 35 | CustomerDeleted = "customer.deleted" // Occurs whenever a customer is deleted. 36 | CustomerUpdated = "customer.updated" // Occurs whenever any property of a customer changes. 37 | CustomerDiscountCreated = "customer.discount.created" // Occurs whenever a coupon is attached to a customer. 38 | CustomerDiscountDeleted = "customer.discount.deleted" // Occurs whenever a customer's discount is removed. 39 | CustomerDiscountUpdated = "customer.discount.updated" // Occurs whenever a customer is switched from one coupon to another. 40 | CustomerSourceCreated = "customer.source.created" // Occurs whenever a new source is created for the customer. 41 | CustomerSourceDeleted = "customer.source.deleted" // Occurs whenever a source is removed from a customer. 42 | CustomerSourceUpdated = "customer.source.updated" // Occurs whenever a source's details are changed. 43 | CustomerSubscriptionCreated = "customer.subscription.created" // Occurs whenever a customer with no subscription is signed up for a plan. 44 | CustomerSubscriptionDeleted = "customer.subscription.deleted" // Occurs whenever a customer ends their subscription. 45 | CustomerSubscriptionTrialWillEnd = "customer.subscription.trial_will_end" // Occurs three days before the trial period of a subscription is scheduled to end. 46 | CustomerSubscriptionUpdated = "customer.subscription.updated" // Occurs whenever a subscription changes. Examples would include switching from one plan to another, or switching status from trial to active. 47 | InvoiceCreated = "invoice.created" // Occurs whenever a new invoice is created. If you are using webhooks, Stripe will wait one hour after they have all succeeded to attempt to pay the invoice; the only exception here is on the first invoice, which gets created and paid immediately when you subscribe a customer to a plan. If your webhooks do not all respond successfully, Stripe will continue retrying the webhooks every hour and will not attempt to pay the invoice. After 3 days, Stripe will attempt to pay the invoice regardless of whether or not your webhooks have succeeded. See how to respond to a webhook. 48 | InvoicePaymentFailed = "invoice.payment_failed" // Occurs whenever an invoice attempts to be paid, and the payment fails. This can occur either due to a declined payment, or because the customer has no active card. A particular case of note is that if a customer with no active card reaches the end of its free trial, an invoice.payment_failed notification will occur. 49 | InvoicePaymentSucceeded = "invoice.payment_succeeded" // Occurs whenever an invoice attempts to be paid, and the payment succeeds. 50 | InvoiceUpdated = "invoice.updated" // Occurs whenever an invoice changes (for example, the amount could change). 51 | InvoiceitemCreated = "invoiceitem.created" // Occurs whenever an invoice item is created. 52 | InvoiceitemDeleted = "invoiceitem.deleted" // Occurs whenever an invoice item is deleted. 53 | InvoiceitemUpdated = "invoiceitem.updated" // Occurs whenever an invoice item is updated. 54 | OrderCreated = "order.created" // Occurs whenever an order is created. 55 | OrderPaymentFailed = "order.payment_failed" // Occurs whenever payment is attempted on an order, and the payment fails. 56 | OrderPaymentSucceeded = "order.payment_succeeded" // Occurs whenever payment is attempted on an order, and the payment succeeds. 57 | OrderUpdated = "order.updated" // Occurs whenever an order is updated. 58 | OrderReturnCreated = "order_return.created" // Occurs whenever an order return created. 59 | PlanCreated = "plan.created" // Occurs whenever a plan is created. 60 | PlanDeleted = "plan.deleted" // Occurs whenever a plan is deleted. 61 | PlanUpdated = "plan.updated" // Occurs whenever a plan is updated. 62 | ProductCreated = "product.created" // Occurs whenever a product is created. 63 | ProductDeleted = "product.deleted" // Occurs whenever a product is deleted. 64 | ProductUpdated = "product.updated" // Occurs whenever a product is updated. 65 | RecipientCreated = "recipient.created" // Occurs whenever a recipient is created. 66 | RecipientDeleted = "recipient.deleted" // Occurs whenever a recipient is deleted. 67 | RecipientUpdated = "recipient.updated" // Occurs whenever a recipient is updated. 68 | SkuCreated = "sku.created" // Occurs whenever a SKU is created. 69 | SkuDeleted = "sku.deleted" // Occurs whenever a SKU is deleted. 70 | SkuUpdated = "sku.updated" // Occurs whenever a SKU is updated. 71 | TransferCreated = "transfer.created" // Occurs whenever a new transfer is created. 72 | TransferFailed = "transfer.failed" // Occurs whenever Stripe attempts to send a transfer and that transfer fails. 73 | TransferPaid = "transfer.paid" // Occurs whenever a sent transfer is expected to be available in the destination bank account. If the transfer failed, a transfer.failed webhook will additionally be sent at a later time. Note to Connect users: this event is only created for transfers from your connected Stripe accounts to their bank accounts, not for transfers to the connected accounts themselves. 74 | TransferReversed = "transfer.reversed" // Occurs whenever a transfer is reversed, including partial reversals. 75 | TransferUpdated = "transfer.updated" // Occurs whenever the description or metadata of a transfer is updated. 76 | Ping = "ping" // May be sent by Stripe at any time to see if a provided webhook URL is working. 77 | ) 78 | -------------------------------------------------------------------------------- /term/term.go: -------------------------------------------------------------------------------- 1 | // Package term provides ansi escape sequence helpers. 2 | package term 3 | 4 | import ( 5 | "fmt" 6 | "math" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/buger/goterm" 11 | "github.com/mattn/go-isatty" 12 | ) 13 | 14 | // Renderer returns a function which renders strings in-place. 15 | // 16 | // The text is rendered to the current cursor position, and when 17 | // cleared with an empty string retains this position as if no 18 | // text has been rendered. 19 | func Renderer() func(string) { 20 | var prev string 21 | 22 | return func(curr string) { 23 | // clear lines 24 | if prev != "" { 25 | for range lines(prev) { 26 | MoveUp(1) 27 | ClearLine() 28 | } 29 | } 30 | 31 | // print lines 32 | if curr != "" { 33 | for _, s := range lines(curr) { 34 | fmt.Printf("%s\n", s) 35 | } 36 | } 37 | 38 | prev = curr 39 | } 40 | } 41 | 42 | // lines returns the lines in the given string. 43 | func lines(s string) []string { 44 | return strings.Split(s, "\n") 45 | } 46 | 47 | // strip regexp. 48 | var strip = regexp.MustCompile(`\x1B\[[0-?]*[ -/]*[@-~]`) 49 | 50 | // Strip ansi escape sequences. 51 | func Strip(s string) string { 52 | return strip.ReplaceAllString(s, "") 53 | } 54 | 55 | // Length of characters with ansi escape sequences stripped. 56 | func Length(s string) (n int) { 57 | for range Strip(s) { 58 | n++ 59 | } 60 | return 61 | } 62 | 63 | // CenterLine a line of text. 64 | func CenterLine(s string) string { 65 | r := strings.Repeat 66 | w, h := Size() 67 | size := Length(s) 68 | xpad := int(math.Abs(float64((w - size) / 2))) 69 | ypad := h / 2 70 | return r("\n", ypad) + r(" ", xpad) + s + r("\n", ypad) 71 | } 72 | 73 | // Size returns the width and height. 74 | func Size() (w int, h int) { 75 | w = goterm.Width() 76 | h = goterm.Height() 77 | return 78 | } 79 | 80 | // ClearAll clears the screen. 81 | func ClearAll() { 82 | fmt.Printf("\033[2J") 83 | MoveTo(1, 1) 84 | } 85 | 86 | // ClearLine clears the entire line. 87 | func ClearLine() { 88 | fmt.Printf("\033[2K") 89 | } 90 | 91 | // ClearLineEnd clears to the end of the line. 92 | func ClearLineEnd() { 93 | fmt.Printf("\033[0K") 94 | } 95 | 96 | // ClearLineStart clears to the start of the line. 97 | func ClearLineStart() { 98 | fmt.Printf("\033[1K") 99 | } 100 | 101 | // MoveTo moves the cursor to (x, y). 102 | func MoveTo(x, y int) { 103 | fmt.Printf("\033[%d;%df", y, x) 104 | } 105 | 106 | // MoveDown moves the cursor to the beginning of n lines down. 107 | func MoveDown(n int) { 108 | fmt.Printf("\033[%dE", n) 109 | } 110 | 111 | // MoveUp moves the cursor to the beginning of n lines up. 112 | func MoveUp(n int) { 113 | fmt.Printf("\033[%dF", n) 114 | } 115 | 116 | // SaveCursorPosition saves the cursor position. 117 | func SaveCursorPosition() { 118 | fmt.Printf("\033[s") 119 | } 120 | 121 | // RestoreCursorPosition saves the cursor position. 122 | func RestoreCursorPosition() { 123 | fmt.Printf("\033[u") 124 | } 125 | 126 | // HideCursor hides the cursor. 127 | func HideCursor() { 128 | fmt.Printf("\033[?25l") 129 | } 130 | 131 | // ShowCursor shows the cursor. 132 | func ShowCursor() { 133 | fmt.Printf("\033[?25h") 134 | } 135 | 136 | // IsTerminal returns true if fd is a tty. 137 | func IsTerminal(fd uintptr) bool { 138 | return isatty.IsTerminal(fd) 139 | } 140 | -------------------------------------------------------------------------------- /term/term_test.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestRenderer(t *testing.T) { 11 | if os.Getenv("TEST_SLOW") == "" { 12 | t.SkipNow() 13 | } 14 | 15 | render := Renderer() 16 | 17 | fmt.Printf("\n\n-----\n") 18 | time.Sleep(time.Second) 19 | 20 | render(`Deploying`) 21 | time.Sleep(time.Second) 22 | 23 | render(`Deploying 24 | - some resource`) 25 | time.Sleep(time.Second) 26 | 27 | render(`Deploying 28 | - some resource (complete) 29 | - another resource`) 30 | time.Sleep(time.Second) 31 | 32 | render(`Deploying 33 | - some resource (complete) 34 | - another resource (complete)`) 35 | time.Sleep(time.Second) 36 | 37 | render(`Deploying 38 | - some resource (complete) 39 | - another resource (complete) 40 | - final resource`) 41 | time.Sleep(time.Second) 42 | 43 | render(`Deploying 44 | - some resource (complete) 45 | - another resource (complete) 46 | - final resource (complete)`) 47 | time.Sleep(time.Second) 48 | 49 | render(`Deployment complete`) 50 | time.Sleep(time.Second * 2) 51 | 52 | render(``) 53 | time.Sleep(time.Second) 54 | 55 | fmt.Printf("-----\n\n\n") 56 | } 57 | --------------------------------------------------------------------------------