mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Add cli/go-gh/v2 and cli/safeexec vendor dependencies
Required for authenticating with GitHub's API using the token stored by the gh CLI.
This commit is contained in:
committed by
Stefan Haller
parent
4cd72b09a2
commit
678a2a90cb
@@ -10,6 +10,7 @@ require (
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cli/go-gh/v2 v2.13.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gdamore/tcell/v2 v2.13.8
|
||||
@@ -35,7 +36,7 @@ require (
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.42.0
|
||||
@@ -46,6 +47,7 @@ require (
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/cli/safeexec v1.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
@@ -57,8 +59,8 @@ require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.34.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
|
||||
|
||||
@@ -11,6 +11,10 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys=
|
||||
github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM=
|
||||
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
|
||||
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -72,13 +76,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
@@ -125,8 +130,9 @@ github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xz
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -152,13 +158,13 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package set
|
||||
|
||||
var exists = struct{}{}
|
||||
|
||||
type stringSet struct {
|
||||
v []string
|
||||
m map[string]struct{}
|
||||
}
|
||||
|
||||
func NewStringSet() *stringSet {
|
||||
s := &stringSet{}
|
||||
s.m = make(map[string]struct{})
|
||||
s.v = []string{}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *stringSet) Add(value string) {
|
||||
if s.Contains(value) {
|
||||
return
|
||||
}
|
||||
s.m[value] = exists
|
||||
s.v = append(s.v, value)
|
||||
}
|
||||
|
||||
func (s *stringSet) AddValues(values []string) {
|
||||
for _, v := range values {
|
||||
s.Add(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringSet) Remove(value string) {
|
||||
if !s.Contains(value) {
|
||||
return
|
||||
}
|
||||
delete(s.m, value)
|
||||
s.v = sliceWithout(s.v, value)
|
||||
}
|
||||
|
||||
func sliceWithout(s []string, v string) []string {
|
||||
idx := -1
|
||||
for i, item := range s {
|
||||
if item == v {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return s
|
||||
}
|
||||
return append(s[:idx], s[idx+1:]...)
|
||||
}
|
||||
|
||||
func (s *stringSet) RemoveValues(values []string) {
|
||||
for _, v := range values {
|
||||
s.Remove(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringSet) Contains(value string) bool {
|
||||
_, c := s.m[value]
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *stringSet) Len() int {
|
||||
return len(s.m)
|
||||
}
|
||||
|
||||
func (s *stringSet) ToSlice() []string {
|
||||
return s.v
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// Package yamlmap is a wrapper of gopkg.in/yaml.v3 for interacting
|
||||
// with yaml data as if it were a map.
|
||||
package yamlmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
modified = "modifed"
|
||||
)
|
||||
|
||||
type Map struct {
|
||||
*yaml.Node
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrInvalidYaml = errors.New("invalid yaml")
|
||||
var ErrInvalidFormat = errors.New("invalid format")
|
||||
|
||||
func StringValue(value string) *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: value,
|
||||
}}
|
||||
}
|
||||
|
||||
func MapValue() *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Tag: "!!map",
|
||||
}}
|
||||
}
|
||||
|
||||
func NullValue() *Map {
|
||||
return &Map{&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!null",
|
||||
}}
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte) (*Map, error) {
|
||||
var root yaml.Node
|
||||
err := yaml.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidYaml
|
||||
}
|
||||
if len(root.Content) == 0 {
|
||||
return MapValue(), nil
|
||||
}
|
||||
if root.Content[0].Kind != yaml.MappingNode {
|
||||
return nil, ErrInvalidFormat
|
||||
}
|
||||
return &Map{root.Content[0]}, nil
|
||||
}
|
||||
|
||||
func Marshal(m *Map) ([]byte, error) {
|
||||
return yaml.Marshal(m.Node)
|
||||
}
|
||||
|
||||
func (m *Map) AddEntry(key string, value *Map) {
|
||||
keyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: key,
|
||||
}
|
||||
m.Content = append(m.Content, keyNode, value.Node)
|
||||
m.SetModified()
|
||||
}
|
||||
|
||||
func (m *Map) Empty() bool {
|
||||
return len(m.Content) == 0
|
||||
}
|
||||
|
||||
func (m *Map) FindEntry(key string) (*Map, error) {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 {
|
||||
continue
|
||||
}
|
||||
if v.Value == key {
|
||||
if i+1 < len(m.Content) {
|
||||
return &Map{m.Content[i+1]}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (m *Map) Keys() []string {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to select the keys of the yamlMap.
|
||||
keys := []string{}
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, v.Value)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (m *Map) RemoveEntry(key string) error {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
// If we find they key to remove, remove the key and its value from the content slice.
|
||||
found, skipNext := false, false
|
||||
newContent := []*yaml.Node{}
|
||||
for i, v := range m.Content {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
continue
|
||||
}
|
||||
if i%2 != 0 || v.Value != key {
|
||||
newContent = append(newContent, v)
|
||||
} else {
|
||||
found = true
|
||||
skipNext = true
|
||||
m.SetModified()
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ErrNotFound
|
||||
}
|
||||
m.Content = newContent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Map) SetEntry(key string, value *Map) {
|
||||
// Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...].
|
||||
// When iterating over the content slice we only want to compare the keys of the yamlMap.
|
||||
// If we find they key to set, set the next item in the content slice to the new value.
|
||||
m.SetModified()
|
||||
for i, v := range m.Content {
|
||||
if i%2 != 0 || v.Value != key {
|
||||
continue
|
||||
}
|
||||
if v.Value == key {
|
||||
if i+1 < len(m.Content) {
|
||||
m.Content[i+1] = value.Node
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.AddEntry(key, value)
|
||||
}
|
||||
|
||||
// SetModified marks the map as modified.
|
||||
//
|
||||
// Note: This is a hack to introduce the concept of modified/unmodified
|
||||
// on top of gopkg.in/yaml.v3. This works by setting the Value property
|
||||
// of a MappingNode to a specific value and then later checking if the
|
||||
// node's Value property is that specific value. When a MappingNode gets
|
||||
// output as a string the Value property is not used, thus changing it
|
||||
// has no impact for our purposes.
|
||||
func (m *Map) SetModified() {
|
||||
// Can not mark a non-mapping node as modified
|
||||
if m.Node.Kind != yaml.MappingNode && m.Node.Tag == "!!null" {
|
||||
m.Node.Kind = yaml.MappingNode
|
||||
m.Node.Tag = "!!map"
|
||||
}
|
||||
if m.Node.Kind == yaml.MappingNode {
|
||||
m.Node.Value = modified
|
||||
}
|
||||
}
|
||||
|
||||
// SetUnmodified traverses the map using BFS to set all nodes as unmodified.
|
||||
func (m *Map) SetUnmodified() {
|
||||
i := 0
|
||||
queue := []*yaml.Node{m.Node}
|
||||
for i < len(queue) {
|
||||
q := queue[i]
|
||||
i = i + 1
|
||||
if q.Kind != yaml.MappingNode {
|
||||
continue
|
||||
}
|
||||
q.Value = ""
|
||||
queue = append(queue, q.Content...)
|
||||
}
|
||||
}
|
||||
|
||||
// IsModified traverses the map using BFS to search for any nodes that have been modified.
|
||||
func (m *Map) IsModified() bool {
|
||||
i := 0
|
||||
queue := []*yaml.Node{m.Node}
|
||||
for i < len(queue) {
|
||||
q := queue[i]
|
||||
i = i + 1
|
||||
if q.Kind != yaml.MappingNode {
|
||||
continue
|
||||
}
|
||||
if q.Value == modified {
|
||||
return true
|
||||
}
|
||||
queue = append(queue, q.Content...)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map) String() string {
|
||||
data, err := Marshal(m)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// Package auth is a set of functions for retrieving authentication tokens
|
||||
// and authenticated hosts.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/go-gh/v2/internal/set"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
const (
|
||||
codespaces = "CODESPACES"
|
||||
defaultSource = "default"
|
||||
ghEnterpriseToken = "GH_ENTERPRISE_TOKEN"
|
||||
ghHost = "GH_HOST"
|
||||
ghToken = "GH_TOKEN"
|
||||
github = "github.com"
|
||||
githubEnterpriseToken = "GITHUB_ENTERPRISE_TOKEN"
|
||||
githubToken = "GITHUB_TOKEN"
|
||||
hostsKey = "hosts"
|
||||
localhost = "github.localhost"
|
||||
oauthToken = "oauth_token"
|
||||
tenancyHost = "ghe.com" // TenancyHost is the domain suffix of a tenancy GitHub instance.
|
||||
)
|
||||
|
||||
// TokenForHost retrieves an authentication token and the source of that token for the specified
|
||||
// host. The source can be either an environment variable, configuration file, or the system
|
||||
// keyring. In the latter case, this shells out to "gh auth token" to obtain the token.
|
||||
//
|
||||
// Returns "", "default" if no applicable token is found.
|
||||
func TokenForHost(host string) (string, string) {
|
||||
if token, source := TokenFromEnvOrConfig(host); token != "" {
|
||||
return token, source
|
||||
}
|
||||
|
||||
ghExe := os.Getenv("GH_PATH")
|
||||
if ghExe == "" {
|
||||
ghExe, _ = safeexec.LookPath("gh")
|
||||
}
|
||||
|
||||
if ghExe != "" {
|
||||
if token, source := tokenFromGh(ghExe, host); token != "" {
|
||||
return token, source
|
||||
}
|
||||
}
|
||||
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
// TokenFromEnvOrConfig retrieves an authentication token from environment variables or the config
|
||||
// file as fallback, but does not support reading the token from system keyring. Most consumers
|
||||
// should use TokenForHost.
|
||||
func TokenFromEnvOrConfig(host string) (string, string) {
|
||||
cfg, _ := config.Read(nil)
|
||||
return tokenForHost(cfg, host)
|
||||
}
|
||||
|
||||
func tokenForHost(cfg *config.Config, host string) (string, string) {
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
// This code is currently the exact opposite of IsEnterprise. However, we have chosen
|
||||
// to write it separately, directly in line, because it is much clearer in the exact
|
||||
// scenarios that we expect to use GH_TOKEN and GITHUB_TOKEN.
|
||||
if normalizedHost == github || IsTenancy(normalizedHost) || normalizedHost == localhost {
|
||||
if token := os.Getenv(ghToken); token != "" {
|
||||
return token, ghToken
|
||||
}
|
||||
|
||||
if token := os.Getenv(githubToken); token != "" {
|
||||
return token, githubToken
|
||||
}
|
||||
} else {
|
||||
if token := os.Getenv(ghEnterpriseToken); token != "" {
|
||||
return token, ghEnterpriseToken
|
||||
}
|
||||
|
||||
if token := os.Getenv(githubEnterpriseToken); token != "" {
|
||||
return token, githubEnterpriseToken
|
||||
}
|
||||
}
|
||||
|
||||
// If config is nil, something has failed much earlier and it's probably
|
||||
// more correct to panic because we don't expect to support anything
|
||||
// where the config isn't available, but that would be a breaking change,
|
||||
// so it's worth thinking about carefully, if we wanted to rework this.
|
||||
if cfg == nil {
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
token, err := cfg.Get([]string{hostsKey, normalizedHost, oauthToken})
|
||||
if err != nil {
|
||||
return "", defaultSource
|
||||
}
|
||||
|
||||
return token, oauthToken
|
||||
}
|
||||
|
||||
func tokenFromGh(path string, host string) (string, string) {
|
||||
cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host)
|
||||
result, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", "gh"
|
||||
}
|
||||
return strings.TrimSpace(string(result)), "gh"
|
||||
}
|
||||
|
||||
// KnownHosts retrieves a list of hosts that have corresponding
|
||||
// authentication tokens, either from environment variables
|
||||
// or from the configuration file.
|
||||
// Returns an empty string slice if no hosts are found.
|
||||
func KnownHosts() []string {
|
||||
cfg, _ := config.Read(nil)
|
||||
return knownHosts(cfg)
|
||||
}
|
||||
|
||||
func knownHosts(cfg *config.Config) []string {
|
||||
hosts := set.NewStringSet()
|
||||
if host := os.Getenv(ghHost); host != "" {
|
||||
hosts.Add(host)
|
||||
}
|
||||
if token, _ := tokenForHost(cfg, github); token != "" {
|
||||
hosts.Add(github)
|
||||
}
|
||||
if cfg != nil {
|
||||
keys, err := cfg.Keys([]string{hostsKey})
|
||||
if err == nil {
|
||||
hosts.AddValues(keys)
|
||||
}
|
||||
}
|
||||
return hosts.ToSlice()
|
||||
}
|
||||
|
||||
// DefaultHost retrieves an authenticated host and the source of host.
|
||||
// The source can be either an environment variable or from the
|
||||
// configuration file.
|
||||
// Returns "github.com", "default" if no viable host is found.
|
||||
func DefaultHost() (string, string) {
|
||||
cfg, _ := config.Read(nil)
|
||||
return defaultHost(cfg)
|
||||
}
|
||||
|
||||
func defaultHost(cfg *config.Config) (string, string) {
|
||||
if host := os.Getenv(ghHost); host != "" {
|
||||
return host, ghHost
|
||||
}
|
||||
if cfg != nil {
|
||||
keys, err := cfg.Keys([]string{hostsKey})
|
||||
if err == nil && len(keys) == 1 {
|
||||
return keys[0], hostsKey
|
||||
}
|
||||
}
|
||||
return github, defaultSource
|
||||
}
|
||||
|
||||
// IsEnterprise determines if a provided host is a GitHub Enterprise Server instance,
|
||||
// rather than GitHub.com, a tenancy GitHub instance, or github.localhost.
|
||||
func IsEnterprise(host string) bool {
|
||||
// Note that if you are making changes here, you should also consider making the equivalent
|
||||
// in tokenForHost, which is the exact opposite of this function.
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost)
|
||||
}
|
||||
|
||||
// IsTenancy determines if a provided host is a tenancy GitHub instance,
|
||||
// rather than GitHub.com or a GitHub Enterprise Server instance.
|
||||
func IsTenancy(host string) bool {
|
||||
normalizedHost := NormalizeHostname(host)
|
||||
return strings.HasSuffix(normalizedHost, "."+tenancyHost)
|
||||
}
|
||||
|
||||
// NormalizeHostname ensures the host matches the values used throughout
|
||||
// the rest of the codebase with respect to hostnames. These are github,
|
||||
// localhost, and tenancyHost.
|
||||
func NormalizeHostname(host string) string {
|
||||
hostname := strings.ToLower(host)
|
||||
if strings.HasSuffix(hostname, "."+github) {
|
||||
return github
|
||||
}
|
||||
if strings.HasSuffix(hostname, "."+localhost) {
|
||||
return localhost
|
||||
}
|
||||
// This has been copied over from the cli/cli NormalizeHostname function
|
||||
// to ensure compatible behaviour but we don't fully understand when or
|
||||
// why it would be useful here. We can't see what harm will come of
|
||||
// duplicating the logic.
|
||||
if before, found := cutSuffix(hostname, "."+tenancyHost); found {
|
||||
idx := strings.LastIndex(before, ".")
|
||||
return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
// Backport strings.CutSuffix from Go 1.20.
|
||||
func cutSuffix(s, suffix string) (string, bool) {
|
||||
if !strings.HasSuffix(s, suffix) {
|
||||
return s, false
|
||||
}
|
||||
return s[:len(s)-len(suffix)], true
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
// Package config is a set of types for interacting with the gh configuration files.
|
||||
// Note: This package is intended for use only in gh, any other use cases are subject
|
||||
// to breakage and non-backwards compatible updates.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/go-gh/v2/internal/yamlmap"
|
||||
)
|
||||
|
||||
const (
|
||||
appData = "AppData"
|
||||
ghConfigDir = "GH_CONFIG_DIR"
|
||||
localAppData = "LocalAppData"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
xdgDataHome = "XDG_DATA_HOME"
|
||||
xdgStateHome = "XDG_STATE_HOME"
|
||||
xdgCacheHome = "XDG_CACHE_HOME"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *Config
|
||||
once sync.Once
|
||||
loadErr error
|
||||
)
|
||||
|
||||
// Config is a in memory representation of the gh configuration files.
|
||||
// It can be thought of as map where entries consist of a key that
|
||||
// correspond to either a string value or a map value, allowing for
|
||||
// multi-level maps.
|
||||
type Config struct {
|
||||
entries *yamlmap.Map
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get a string value from a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be retrieved. A undefined string will be returned
|
||||
// if trying to retrieve a key that corresponds to a map value.
|
||||
// Returns "", KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Get(keys []string) (string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
m := c.entries
|
||||
for _, key := range keys {
|
||||
var err error
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return "", &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
return m.Value, nil
|
||||
}
|
||||
|
||||
// Keys enumerates a Config's keys.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// map values can be have their keys enumerated.
|
||||
// Returns nil, KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Keys(keys []string) ([]string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
m := c.entries
|
||||
for _, key := range keys {
|
||||
var err error
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return nil, &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
return m.Keys(), nil
|
||||
}
|
||||
|
||||
// Remove an entry from a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be removed. Removing an entry that has nested
|
||||
// entries removes those also.
|
||||
// Returns KeyNotFoundError if any of the keys can not be found.
|
||||
func (c *Config) Remove(keys []string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
m := c.entries
|
||||
for i := 0; i < len(keys)-1; i++ {
|
||||
var err error
|
||||
key := keys[i]
|
||||
m, err = m.FindEntry(key)
|
||||
if err != nil {
|
||||
return &KeyNotFoundError{key}
|
||||
}
|
||||
}
|
||||
err := m.RemoveEntry(keys[len(keys)-1])
|
||||
if err != nil {
|
||||
return &KeyNotFoundError{keys[len(keys)-1]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set a string value in a Config.
|
||||
// The keys argument is a sequence of key values so that nested
|
||||
// entries can be set. If any of the keys do not exist they will
|
||||
// be created. If the string value to be set is empty it will be
|
||||
// represented as null not an empty string when written.
|
||||
//
|
||||
// var c *Config
|
||||
// c.Set([]string{"key"}, "")
|
||||
// Write(c) // writes `key: ` not `key: ""`
|
||||
func (c *Config) Set(keys []string, value string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
m := c.entries
|
||||
for i := 0; i < len(keys)-1; i++ {
|
||||
key := keys[i]
|
||||
entry, err := m.FindEntry(key)
|
||||
if err != nil {
|
||||
entry = yamlmap.MapValue()
|
||||
m.AddEntry(key, entry)
|
||||
}
|
||||
m = entry
|
||||
}
|
||||
val := yamlmap.StringValue(value)
|
||||
if value == "" {
|
||||
val = yamlmap.NullValue()
|
||||
}
|
||||
m.SetEntry(keys[len(keys)-1], val)
|
||||
}
|
||||
|
||||
func (c *Config) deepCopy() *Config {
|
||||
return ReadFromString(c.entries.String())
|
||||
}
|
||||
|
||||
// Read gh configuration files from the local file system and
|
||||
// returns a Config. A copy of the fallback configuration will
|
||||
// be returned when there are no configuration files to load.
|
||||
// If there are no configuration files and no fallback configuration
|
||||
// an empty configuration will be returned.
|
||||
var Read = func(fallback *Config) (*Config, error) {
|
||||
once.Do(func() {
|
||||
cfg, loadErr = load(generalConfigFile(), hostsConfigFile(), fallback)
|
||||
})
|
||||
return cfg, loadErr
|
||||
}
|
||||
|
||||
// ReadFromString takes a yaml string and returns a Config.
|
||||
func ReadFromString(str string) *Config {
|
||||
m, _ := mapFromString(str)
|
||||
if m == nil {
|
||||
m = yamlmap.MapValue()
|
||||
}
|
||||
return &Config{entries: m}
|
||||
}
|
||||
|
||||
// Write gh configuration files to the local file system.
|
||||
// It will only write gh configuration files that have been modified
|
||||
// since last being read.
|
||||
func Write(c *Config) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
hosts, err := c.entries.FindEntry("hosts")
|
||||
if err == nil && hosts.IsModified() {
|
||||
err := writeFile(hostsConfigFile(), []byte(hosts.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts.SetUnmodified()
|
||||
}
|
||||
|
||||
if c.entries.IsModified() {
|
||||
// Hosts gets written to a different file above so remove it
|
||||
// before writing and add it back in after writing.
|
||||
hostsMap, hostsErr := c.entries.FindEntry("hosts")
|
||||
if hostsErr == nil {
|
||||
_ = c.entries.RemoveEntry("hosts")
|
||||
}
|
||||
err := writeFile(generalConfigFile(), []byte(c.entries.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.entries.SetUnmodified()
|
||||
if hostsErr == nil {
|
||||
c.entries.AddEntry("hosts", hostsMap)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func load(generalFilePath, hostsFilePath string, fallback *Config) (*Config, error) {
|
||||
generalMap, err := mapFromFile(generalFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
|
||||
errors.Is(err, yamlmap.ErrInvalidFormat) {
|
||||
return nil, &InvalidConfigFileError{Path: generalFilePath, Err: err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if generalMap == nil {
|
||||
generalMap = yamlmap.MapValue()
|
||||
}
|
||||
|
||||
hostsMap, err := mapFromFile(hostsFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
if errors.Is(err, yamlmap.ErrInvalidYaml) ||
|
||||
errors.Is(err, yamlmap.ErrInvalidFormat) {
|
||||
return nil, &InvalidConfigFileError{Path: hostsFilePath, Err: err}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hostsMap != nil && !hostsMap.Empty() {
|
||||
generalMap.AddEntry("hosts", hostsMap)
|
||||
generalMap.SetUnmodified()
|
||||
}
|
||||
|
||||
if generalMap.Empty() && fallback != nil {
|
||||
return fallback.deepCopy(), nil
|
||||
}
|
||||
|
||||
return &Config{entries: generalMap}, nil
|
||||
}
|
||||
|
||||
func generalConfigFile() string {
|
||||
return filepath.Join(ConfigDir(), "config.yml")
|
||||
}
|
||||
|
||||
func hostsConfigFile() string {
|
||||
return filepath.Join(ConfigDir(), "hosts.yml")
|
||||
}
|
||||
|
||||
func mapFromFile(filename string) (*yamlmap.Map, error) {
|
||||
data, err := readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yamlmap.Unmarshal(data)
|
||||
}
|
||||
|
||||
func mapFromString(str string) (*yamlmap.Map, error) {
|
||||
return yamlmap.Unmarshal([]byte(str))
|
||||
}
|
||||
|
||||
// ConfigDir returns the path to the configuration directory.
|
||||
//
|
||||
// Config path precedence: GH_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME.
|
||||
func ConfigDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(ghConfigDir); a != "" {
|
||||
path = a
|
||||
} else if b := os.Getenv(xdgConfigHome); b != "" {
|
||||
path = filepath.Join(b, "gh")
|
||||
} else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" {
|
||||
path = filepath.Join(c, "GitHub CLI")
|
||||
} else {
|
||||
d, _ := os.UserHomeDir()
|
||||
path = filepath.Join(d, ".config", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// StateDir returns the path to the state directory.
|
||||
//
|
||||
// State path precedence: XDG_STATE_HOME, LocalAppData (windows only), HOME.
|
||||
func StateDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(xdgStateHome); a != "" {
|
||||
path = filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
path = filepath.Join(b, "GitHub CLI")
|
||||
} else {
|
||||
c, _ := os.UserHomeDir()
|
||||
path = filepath.Join(c, ".local", "state", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// DataDir returns the path to the data directory.
|
||||
//
|
||||
// Data path precedence: XDG_DATA_HOME, LocalAppData (windows only), HOME.
|
||||
func DataDir() string {
|
||||
var path string
|
||||
if a := os.Getenv(xdgDataHome); a != "" {
|
||||
path = filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
path = filepath.Join(b, "GitHub CLI")
|
||||
} else {
|
||||
c, _ := os.UserHomeDir()
|
||||
path = filepath.Join(c, ".local", "share", "gh")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// CacheDir returns the path to the cache directory.
|
||||
//
|
||||
// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME, legacy gh-cli-cache.
|
||||
func CacheDir() string {
|
||||
if a := os.Getenv(xdgCacheHome); a != "" {
|
||||
return filepath.Join(a, "gh")
|
||||
} else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" {
|
||||
return filepath.Join(b, "GitHub CLI")
|
||||
} else if c, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(c, ".cache", "gh")
|
||||
} else {
|
||||
// Note that this has a minor security issue because /tmp is world-writeable.
|
||||
// As such, it is possible for other users on a shared system to overwrite cached data.
|
||||
// The practical risk of this is low, but it's worth calling out as a risk.
|
||||
// I've included this here for backwards compatibility but we should consider removing it.
|
||||
return filepath.Join(os.TempDir(), "gh-cli-cache")
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(filename string) ([]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func writeFile(filename string, data []byte) (writeErr error) {
|
||||
if writeErr = os.MkdirAll(filepath.Dir(filename), 0771); writeErr != nil {
|
||||
return
|
||||
}
|
||||
var file *os.File
|
||||
if file, writeErr = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600); writeErr != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); writeErr == nil && err != nil {
|
||||
writeErr = err
|
||||
}
|
||||
}()
|
||||
_, writeErr = file.Write(data)
|
||||
return
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// InvalidConfigFileError represents an error when trying to read a config file.
|
||||
type InvalidConfigFileError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error allows InvalidConfigFileError to satisfy error interface.
|
||||
func (e *InvalidConfigFileError) Error() string {
|
||||
return fmt.Sprintf("invalid config file %s: %s", e.Path, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap allows InvalidConfigFileError to be unwrapped.
|
||||
func (e *InvalidConfigFileError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// KeyNotFoundError represents an error when trying to find a config key
|
||||
// that does not exist.
|
||||
type KeyNotFoundError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
// Error allows KeyNotFoundError to satisfy error interface.
|
||||
func (e *KeyNotFoundError) Error() string {
|
||||
return fmt.Sprintf("could not find key %q", e.Key)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2020, GitHub Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# safeexec
|
||||
|
||||
A Go module that provides a stabler alternative to `exec.LookPath()` that:
|
||||
- Avoids a Windows security risk of executing commands found in the current directory; and
|
||||
- Allows executing commands found in PATH, even if they come from relative PATH entries.
|
||||
|
||||
This is an alternative to [`golang.org/x/sys/execabs`](https://pkg.go.dev/golang.org/x/sys/execabs).
|
||||
|
||||
## Usage
|
||||
```go
|
||||
import (
|
||||
"os/exec"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
func gitStatus() error {
|
||||
gitBin, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(gitBin, "status")
|
||||
return cmd.Run()
|
||||
}
|
||||
```
|
||||
|
||||
## Background
|
||||
### Windows security vulnerability with Go <= 1.18
|
||||
Go 1.18 (and older) standard library has a security vulnerability when executing programs:
|
||||
```go
|
||||
import "os/exec"
|
||||
|
||||
func gitStatus() error {
|
||||
// On Windows, this will result in `.\git.exe` or `.\git.bat` being executed
|
||||
// if either were found in the current working directory.
|
||||
cmd := exec.Command("git", "status")
|
||||
return cmd.Run()
|
||||
}
|
||||
```
|
||||
|
||||
For historic reasons, Go used to implicitly [include the current directory](https://github.com/golang/go/issues/38736) in the PATH resolution on Windows. The `safeexec` package avoids searching the current directory on Windows.
|
||||
|
||||
### Relative PATH entries with Go 1.19+
|
||||
|
||||
Go 1.19 (and newer) standard library [throws an error](https://github.com/golang/go/issues/43724) if `exec.LookPath("git")` resolved to an executable relative to the current directory. This can happen on other platforms if the PATH environment variable contains relative entries, e.g. `PATH=./bin:$PATH`. The `safeexec` package allows respecting relative PATH entries as it assumes that the responsibility for keeping PATH safe lies outside of the Go program.
|
||||
|
||||
## TODO
|
||||
|
||||
Ideally, this module would also provide `exec.Command()` and `exec.CommandContext()` equivalents that delegate to the patched version of `LookPath`. However, this doesn't seem possible since `LookPath` may return an error, while `exec.Command/CommandContext()` themselves do not return an error. In the standard library, the resulting `exec.Cmd` struct stores the LookPath error in a private field, but that functionality isn't available to us.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
//go:build !windows && go1.19
|
||||
// +build !windows,go1.19
|
||||
|
||||
package safeexec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
path, err := exec.LookPath(file)
|
||||
if errors.Is(err, exec.ErrDot) {
|
||||
return path, nil
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//go:build !windows && !go1.19
|
||||
// +build !windows,!go1.19
|
||||
|
||||
package safeexec
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// Package safeexec provides alternatives for exec package functions to avoid
|
||||
// accidentally executing binaries found in the current working directory on
|
||||
// Windows.
|
||||
package safeexec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func chkStat(file string) error {
|
||||
d, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasExt(file string) bool {
|
||||
i := strings.LastIndex(file, ".")
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
return strings.LastIndexAny(file, `:\/`) < i
|
||||
}
|
||||
|
||||
func findExecutable(file string, exts []string) (string, error) {
|
||||
if len(exts) == 0 {
|
||||
return file, chkStat(file)
|
||||
}
|
||||
if hasExt(file) {
|
||||
if chkStat(file) == nil {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
for _, e := range exts {
|
||||
if f := file + e; chkStat(f) == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the PATH environment variable.
|
||||
// If file contains a slash, it is tried directly and the PATH is not consulted.
|
||||
// LookPath also uses PATHEXT environment variable to match
|
||||
// a suitable candidate.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
var exts []string
|
||||
x := os.Getenv(`PATHEXT`)
|
||||
if x != "" {
|
||||
for _, e := range strings.Split(strings.ToLower(x), `;`) {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if e[0] != '.' {
|
||||
e = "." + e
|
||||
}
|
||||
exts = append(exts, e)
|
||||
}
|
||||
} else {
|
||||
exts = []string{".com", ".exe", ".bat", ".cmd"}
|
||||
}
|
||||
|
||||
if strings.ContainsAny(file, `:\/`) {
|
||||
if f, err := findExecutable(file, exts); err == nil {
|
||||
return f, nil
|
||||
} else {
|
||||
return "", &exec.Error{file, err}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/golang/go/issues/38736
|
||||
// if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
|
||||
// return f, nil
|
||||
// }
|
||||
|
||||
path := os.Getenv("path")
|
||||
for _, dir := range filepath.SplitList(path) {
|
||||
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", &exec.Error{file, exec.ErrNotFound}
|
||||
}
|
||||
-2
@@ -42,7 +42,6 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
c, err := er.ReadByte()
|
||||
if err != nil {
|
||||
@@ -51,7 +50,6 @@ loop:
|
||||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||
break
|
||||
}
|
||||
buf.Write([]byte(string(c)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
|
||||
// +build darwin freebsd openbsd netbsd dragonfly
|
||||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
|
||||
// +build darwin freebsd openbsd netbsd dragonfly hurd
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
//go:build appengine || js || nacl || wasm
|
||||
// +build appengine js nacl wasm
|
||||
//go:build (appengine || js || nacl || tinygo || wasm) && !windows
|
||||
// +build appengine js nacl tinygo wasm
|
||||
// +build !windows
|
||||
|
||||
package isatty
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
//go:build (linux || aix || zos) && !appengine
|
||||
//go:build (linux || aix || zos) && !appengine && !tinygo
|
||||
// +build linux aix zos
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
package terminfo
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// BoolCapName returns the bool capability name.
|
||||
func BoolCapName(i int) string {
|
||||
return boolCapNames[2*i]
|
||||
|
||||
+1
-499
File diff suppressed because it is too large
Load Diff
-3
@@ -70,14 +70,12 @@ func ColorLevelFromEnv() (ColorLevel, error) {
|
||||
}
|
||||
return ColorLevelHundreds, nil
|
||||
}
|
||||
|
||||
// otherwise determine from TERM's max_colors capability
|
||||
if term := os.Getenv("TERM"); term != "" {
|
||||
ti, err := Load(term)
|
||||
if err != nil {
|
||||
return ColorLevelNone, err
|
||||
}
|
||||
|
||||
v, ok := ti.Nums[MaxColors]
|
||||
switch {
|
||||
case !ok || v <= 16:
|
||||
@@ -86,6 +84,5 @@ func ColorLevelFromEnv() (ColorLevel, error) {
|
||||
return ColorLevelHundreds, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ColorLevelBasic, nil
|
||||
}
|
||||
|
||||
+12
-33
@@ -7,12 +7,11 @@ import (
|
||||
const (
|
||||
// maxFileLength is the max file length.
|
||||
maxFileLength = 4096
|
||||
|
||||
// magic is the file magic for terminfo files.
|
||||
magic = 0432
|
||||
|
||||
// magicExtended is the file magic for terminfo files with the extended number format.
|
||||
magicExtended = 01036
|
||||
magic = 0o432
|
||||
// magicExtended is the file magic for terminfo files with the extended
|
||||
// number format.
|
||||
magicExtended = 0o1036
|
||||
)
|
||||
|
||||
// header fields.
|
||||
@@ -99,12 +98,12 @@ func readStrings(idx []int, buf []byte, n int) (map[int][]byte, int, error) {
|
||||
type decoder struct {
|
||||
buf []byte
|
||||
pos int
|
||||
len int
|
||||
n int
|
||||
}
|
||||
|
||||
// readBytes reads the next n bytes of buf, incrementing pos by n.
|
||||
func (d *decoder) readBytes(n int) ([]byte, error) {
|
||||
if d.len < d.pos+n {
|
||||
if d.n < d.pos+n {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
n, d.pos = d.pos, d.pos+n
|
||||
@@ -115,15 +114,12 @@ func (d *decoder) readBytes(n int) ([]byte, error) {
|
||||
func (d *decoder) readInts(n, w int) ([]int, error) {
|
||||
w /= 8
|
||||
l := n * w
|
||||
|
||||
buf, err := d.readBytes(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// align
|
||||
d.pos += d.pos % 2
|
||||
|
||||
z := make([]int, n)
|
||||
for i, j := 0, 0; i < l; i, j = i+w, j+1 {
|
||||
switch w {
|
||||
@@ -135,7 +131,6 @@ func (d *decoder) readInts(n, w int) ([]int, error) {
|
||||
z[j] = int(buf[i+3])<<24 | int(buf[i+2])<<16 | int(buf[i+1])<<8 | int(buf[i])
|
||||
}
|
||||
}
|
||||
|
||||
return z, nil
|
||||
}
|
||||
|
||||
@@ -145,7 +140,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// process
|
||||
bools, boolsM := make(map[int]bool), make(map[int]bool)
|
||||
for i, b := range buf {
|
||||
@@ -154,7 +148,6 @@ func (d *decoder) readBools(n int) (map[int]bool, map[int]bool, error) {
|
||||
boolsM[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
return bools, boolsM, nil
|
||||
}
|
||||
|
||||
@@ -164,7 +157,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// process
|
||||
nums, numsM := make(map[int]int), make(map[int]bool)
|
||||
for i := 0; i < n; i++ {
|
||||
@@ -173,7 +165,6 @@ func (d *decoder) readNums(n, w int) (map[int]int, map[int]bool, error) {
|
||||
numsM[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
return nums, numsM, nil
|
||||
}
|
||||
|
||||
@@ -184,16 +175,13 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// read string data table
|
||||
data, err := d.readBytes(sz)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// align
|
||||
d.pos += d.pos % 2
|
||||
|
||||
// process
|
||||
s := make([][]byte, n)
|
||||
var m []int
|
||||
@@ -209,7 +197,6 @@ func (d *decoder) readStringTable(n, sz int) ([][]byte, []int, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, m, nil
|
||||
}
|
||||
|
||||
@@ -220,7 +207,6 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
strs := make(map[int][]byte)
|
||||
for k, v := range s {
|
||||
if k == AcsChars {
|
||||
@@ -228,39 +214,32 @@ func (d *decoder) readStrings(n, sz int) (map[int][]byte, map[int]bool, error) {
|
||||
}
|
||||
strs[k] = v
|
||||
}
|
||||
|
||||
strsM := make(map[int]bool, len(m))
|
||||
for _, k := range m {
|
||||
strsM[k] = true
|
||||
}
|
||||
|
||||
return strs, strsM, nil
|
||||
}
|
||||
|
||||
// canonicalizeAscChars reorders chars to be unique, in order.
|
||||
//
|
||||
// see repair_ascc in ncurses-6.0/progs/dump_entry.c
|
||||
// see repair_ascc in ncurses-6.3/progs/dump_entry.c
|
||||
func canonicalizeAscChars(z []byte) []byte {
|
||||
var c chars
|
||||
var c []byte
|
||||
enc := make(map[byte]byte, len(z)/2)
|
||||
for i := 0; i < len(z); i += 2 {
|
||||
if _, ok := enc[z[i]]; !ok {
|
||||
a, b := z[i], z[i+1]
|
||||
//log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
|
||||
// log.Printf(">>> a: %d %c, b: %d %c", a, a, b, b)
|
||||
c, enc[a] = append(c, b), b
|
||||
}
|
||||
}
|
||||
sort.Sort(c)
|
||||
|
||||
sort.Slice(c, func(i, j int) bool {
|
||||
return c[i] < c[j]
|
||||
})
|
||||
r := make([]byte, 2*len(c))
|
||||
for i := 0; i < len(c); i++ {
|
||||
r[i*2], r[i*2+1] = c[i], enc[c[i]]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type chars []byte
|
||||
|
||||
func (c chars) Len() int { return len(c) }
|
||||
func (c chars) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
func (c chars) Less(i, j int) bool { return c[i] < c[j] }
|
||||
-8
@@ -23,34 +23,27 @@ func Load(name string) (*Terminfo, error) {
|
||||
if name == "" {
|
||||
return nil, ErrEmptyTermName
|
||||
}
|
||||
|
||||
termCache.RLock()
|
||||
ti, ok := termCache.db[name]
|
||||
termCache.RUnlock()
|
||||
|
||||
if ok {
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
var checkDirs []string
|
||||
|
||||
// check $TERMINFO
|
||||
if dir := os.Getenv("TERMINFO"); dir != "" {
|
||||
checkDirs = append(checkDirs, dir)
|
||||
}
|
||||
|
||||
// check $HOME/.terminfo
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkDirs = append(checkDirs, path.Join(u.HomeDir, ".terminfo"))
|
||||
|
||||
// check $TERMINFO_DIRS
|
||||
if dirs := os.Getenv("TERMINFO_DIRS"); dirs != "" {
|
||||
checkDirs = append(checkDirs, strings.Split(dirs, ":")...)
|
||||
}
|
||||
|
||||
// check fallback directories
|
||||
checkDirs = append(checkDirs, "/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo")
|
||||
for _, dir := range checkDirs {
|
||||
@@ -61,7 +54,6 @@ func Load(name string) (*Terminfo, error) {
|
||||
return ti, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrDatabaseDirectoryNotFound
|
||||
}
|
||||
|
||||
|
||||
-85
@@ -13,25 +13,18 @@ import (
|
||||
type parametizer struct {
|
||||
// z is the string to parameterize
|
||||
z []byte
|
||||
|
||||
// pos is the current position in s.
|
||||
pos int
|
||||
|
||||
// nest is the current nest level.
|
||||
nest int
|
||||
|
||||
// s is the variable stack.
|
||||
s stack
|
||||
|
||||
// skipElse keeps the state of skipping else.
|
||||
skipElse bool
|
||||
|
||||
// buf is the result buffer.
|
||||
buf *bytes.Buffer
|
||||
|
||||
// params are the parameters to interpolate.
|
||||
params [9]interface{}
|
||||
|
||||
// vars are dynamic variables.
|
||||
vars [26]interface{}
|
||||
}
|
||||
@@ -54,19 +47,15 @@ var parametizerPool = sync.Pool{
|
||||
func newParametizer(z []byte) *parametizer {
|
||||
p := parametizerPool.Get().(*parametizer)
|
||||
p.z = z
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// reset resets the parametizer.
|
||||
func (p *parametizer) reset() {
|
||||
p.pos, p.nest = 0, 0
|
||||
|
||||
p.s.reset()
|
||||
p.buf.Reset()
|
||||
|
||||
p.params, p.vars = [9]interface{}{}, [26]interface{}{}
|
||||
|
||||
parametizerPool.Put(p)
|
||||
}
|
||||
|
||||
@@ -106,13 +95,11 @@ func (p *parametizer) scanTextFn() stateFn {
|
||||
p.writeFrom(ppos)
|
||||
return nil
|
||||
}
|
||||
|
||||
if ch == '%' {
|
||||
p.writeFrom(ppos)
|
||||
p.pos++
|
||||
return p.scanCodeFn
|
||||
}
|
||||
|
||||
p.pos++
|
||||
}
|
||||
}
|
||||
@@ -122,11 +109,9 @@ func (p *parametizer) scanCodeFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '%':
|
||||
p.buf.WriteByte('%')
|
||||
|
||||
case ':':
|
||||
// this character is used to avoid interpreting "%-" and "%+" as operators.
|
||||
// the next character is where the format really begins.
|
||||
@@ -136,71 +121,52 @@ func (p *parametizer) scanCodeFn() stateFn {
|
||||
return nil
|
||||
}
|
||||
return p.scanFormatFn
|
||||
|
||||
case '#', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
|
||||
return p.scanFormatFn
|
||||
|
||||
case 'o':
|
||||
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 8))
|
||||
|
||||
case 'd':
|
||||
p.buf.WriteString(strconv.Itoa(p.s.popInt()))
|
||||
|
||||
case 'x':
|
||||
p.buf.WriteString(strconv.FormatInt(int64(p.s.popInt()), 16))
|
||||
|
||||
case 'X':
|
||||
p.buf.WriteString(strings.ToUpper(strconv.FormatInt(int64(p.s.popInt()), 16)))
|
||||
|
||||
case 's':
|
||||
p.buf.WriteString(p.s.popString())
|
||||
|
||||
case 'c':
|
||||
p.buf.WriteByte(p.s.popByte())
|
||||
|
||||
case 'p':
|
||||
p.pos++
|
||||
return p.pushParamFn
|
||||
|
||||
case 'P':
|
||||
p.pos++
|
||||
return p.setDsVarFn
|
||||
|
||||
case 'g':
|
||||
p.pos++
|
||||
return p.getDsVarFn
|
||||
|
||||
case '\'':
|
||||
p.pos++
|
||||
ch, err = p.peek()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.s.push(ch)
|
||||
|
||||
// skip the '\''
|
||||
p.pos++
|
||||
|
||||
case '{':
|
||||
p.pos++
|
||||
return p.pushIntfn
|
||||
|
||||
case 'l':
|
||||
p.s.push(len(p.s.popString()))
|
||||
|
||||
case '+':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai + bi)
|
||||
|
||||
case '-':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai - bi)
|
||||
|
||||
case '*':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai * bi)
|
||||
|
||||
case '/':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
if bi != 0 {
|
||||
@@ -208,7 +174,6 @@ func (p *parametizer) scanCodeFn() stateFn {
|
||||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
case 'm':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
if bi != 0 {
|
||||
@@ -216,101 +181,77 @@ func (p *parametizer) scanCodeFn() stateFn {
|
||||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
case '&':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai & bi)
|
||||
|
||||
case '|':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai | bi)
|
||||
|
||||
case '^':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai ^ bi)
|
||||
|
||||
case '=':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai == bi)
|
||||
|
||||
case '>':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai > bi)
|
||||
|
||||
case '<':
|
||||
bi, ai := p.s.popInt(), p.s.popInt()
|
||||
p.s.push(ai < bi)
|
||||
|
||||
case 'A':
|
||||
bi, ai := p.s.popBool(), p.s.popBool()
|
||||
p.s.push(ai && bi)
|
||||
|
||||
case 'O':
|
||||
bi, ai := p.s.popBool(), p.s.popBool()
|
||||
p.s.push(ai || bi)
|
||||
|
||||
case '!':
|
||||
p.s.push(!p.s.popBool())
|
||||
|
||||
case '~':
|
||||
p.s.push(^p.s.popInt())
|
||||
|
||||
case 'i':
|
||||
for i := range p.params[:2] {
|
||||
if n, ok := p.params[i].(int); ok {
|
||||
p.params[i] = n + 1
|
||||
}
|
||||
}
|
||||
|
||||
case '?', ';':
|
||||
|
||||
case 't':
|
||||
return p.scanThenFn
|
||||
|
||||
case 'e':
|
||||
p.skipElse = true
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
func (p *parametizer) scanFormatFn() stateFn {
|
||||
// the character was already read, so no need to check the error.
|
||||
ch, _ := p.peek()
|
||||
|
||||
// 6 should be the maximum length of a format string, for example "%:-9.9d".
|
||||
f := []byte{'%', ch, 0, 0, 0, 0}
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
p.pos++
|
||||
ch, err = p.peek()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f = append(f, ch)
|
||||
switch ch {
|
||||
case 'o', 'd', 'x', 'X':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popInt())
|
||||
break
|
||||
|
||||
case 's':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popString())
|
||||
break
|
||||
|
||||
case 'c':
|
||||
fmt.Fprintf(p.buf, string(f), p.s.popByte())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
@@ -319,16 +260,13 @@ func (p *parametizer) pushParamFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ai := int(ch - '1'); ai >= 0 && ai < len(p.params) {
|
||||
p.s.push(p.params[ai])
|
||||
} else {
|
||||
p.s.push(0)
|
||||
}
|
||||
|
||||
// skip the '}'
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
@@ -337,7 +275,6 @@ func (p *parametizer) setDsVarFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
staticVars.Lock()
|
||||
staticVars.vars[int(ch-'A')] = p.s.pop()
|
||||
@@ -345,7 +282,6 @@ func (p *parametizer) setDsVarFn() stateFn {
|
||||
} else if ch >= 'a' && ch <= 'z' {
|
||||
p.vars[int(ch-'a')] = p.s.pop()
|
||||
}
|
||||
|
||||
p.pos++
|
||||
return p.scanTextFn
|
||||
}
|
||||
@@ -355,20 +291,16 @@ func (p *parametizer) getDsVarFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var a byte
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
a = 'A'
|
||||
} else if ch >= 'a' && ch <= 'z' {
|
||||
a = 'a'
|
||||
}
|
||||
|
||||
staticVars.Lock()
|
||||
p.s.push(staticVars.vars[int(ch-a)])
|
||||
staticVars.Unlock()
|
||||
|
||||
p.pos++
|
||||
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
@@ -379,26 +311,21 @@ func (p *parametizer) pushIntfn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
if ch < '0' || ch > '9' {
|
||||
p.s.push(ai)
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
ai = (ai * 10) + int(ch-'0')
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parametizer) scanThenFn() stateFn {
|
||||
p.pos++
|
||||
|
||||
if p.s.popBool() {
|
||||
return p.scanTextFn
|
||||
}
|
||||
|
||||
p.skipElse = false
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
@@ -408,17 +335,14 @@ func (p *parametizer) skipTextFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
if ch == '%' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.skipElse {
|
||||
return p.skipElseFn
|
||||
}
|
||||
|
||||
return p.skipThenFn
|
||||
}
|
||||
|
||||
@@ -427,7 +351,6 @@ func (p *parametizer) skipThenFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
switch ch {
|
||||
case ';':
|
||||
@@ -435,16 +358,13 @@ func (p *parametizer) skipThenFn() stateFn {
|
||||
return p.scanTextFn
|
||||
}
|
||||
p.nest--
|
||||
|
||||
case '?':
|
||||
p.nest++
|
||||
|
||||
case 'e':
|
||||
if p.nest == 0 {
|
||||
return p.scanTextFn
|
||||
}
|
||||
}
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
@@ -453,7 +373,6 @@ func (p *parametizer) skipElseFn() stateFn {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.pos++
|
||||
switch ch {
|
||||
case ';':
|
||||
@@ -461,11 +380,9 @@ func (p *parametizer) skipElseFn() stateFn {
|
||||
return p.scanTextFn
|
||||
}
|
||||
p.nest--
|
||||
|
||||
case '?':
|
||||
p.nest++
|
||||
}
|
||||
|
||||
return p.skipTextFn
|
||||
}
|
||||
|
||||
@@ -473,13 +390,11 @@ func (p *parametizer) skipElseFn() stateFn {
|
||||
func Printf(z []byte, params ...interface{}) string {
|
||||
p := newParametizer(z)
|
||||
defer p.reset()
|
||||
|
||||
// make sure we always have 9 parameters -- makes it easier
|
||||
// later to skip checks and its faster
|
||||
for i := 0; i < len(p.params) && i < len(params); i++ {
|
||||
p.params[i] = params[i]
|
||||
}
|
||||
|
||||
return p.exec()
|
||||
}
|
||||
|
||||
|
||||
+12
-71
@@ -1,6 +1,8 @@
|
||||
// Package terminfo implements reading terminfo files in pure go.
|
||||
package terminfo
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -20,34 +22,24 @@ func (err Error) Error() string {
|
||||
const (
|
||||
// ErrInvalidFileSize is the invalid file size error.
|
||||
ErrInvalidFileSize Error = "invalid file size"
|
||||
|
||||
// ErrUnexpectedFileEnd is the unexpected file end error.
|
||||
ErrUnexpectedFileEnd Error = "unexpected file end"
|
||||
|
||||
// ErrInvalidStringTable is the invalid string table error.
|
||||
ErrInvalidStringTable Error = "invalid string table"
|
||||
|
||||
// ErrInvalidMagic is the invalid magic error.
|
||||
ErrInvalidMagic Error = "invalid magic"
|
||||
|
||||
// ErrInvalidHeader is the invalid header error.
|
||||
ErrInvalidHeader Error = "invalid header"
|
||||
|
||||
// ErrInvalidNames is the invalid names error.
|
||||
ErrInvalidNames Error = "invalid names"
|
||||
|
||||
// ErrInvalidExtendedHeader is the invalid extended header error.
|
||||
ErrInvalidExtendedHeader Error = "invalid extended header"
|
||||
|
||||
// ErrEmptyTermName is the empty term name error.
|
||||
ErrEmptyTermName Error = "empty term name"
|
||||
|
||||
// ErrDatabaseDirectoryNotFound is the database directory not found error.
|
||||
ErrDatabaseDirectoryNotFound Error = "database directory not found"
|
||||
|
||||
// ErrFileNotFound is the file not found error.
|
||||
ErrFileNotFound Error = "file not found"
|
||||
|
||||
// ErrInvalidTermProgramVersion is the invalid TERM_PROGRAM_VERSION error.
|
||||
ErrInvalidTermProgramVersion Error = "invalid TERM_PROGRAM_VERSION"
|
||||
)
|
||||
@@ -56,43 +48,30 @@ const (
|
||||
type Terminfo struct {
|
||||
// File is the original source file.
|
||||
File string
|
||||
|
||||
// Names are the provided cap names.
|
||||
Names []string
|
||||
|
||||
// Bools are the bool capabilities.
|
||||
Bools map[int]bool
|
||||
|
||||
// BoolsM are the missing bool capabilities.
|
||||
BoolsM map[int]bool
|
||||
|
||||
// Nums are the num capabilities.
|
||||
Nums map[int]int
|
||||
|
||||
// NumsM are the missing num capabilities.
|
||||
NumsM map[int]bool
|
||||
|
||||
// Strings are the string capabilities.
|
||||
Strings map[int][]byte
|
||||
|
||||
// StringsM are the missing string capabilities.
|
||||
StringsM map[int]bool
|
||||
|
||||
// ExtBools are the extended bool capabilities.
|
||||
ExtBools map[int]bool
|
||||
|
||||
// ExtBoolsNames is the map of extended bool capabilities to their index.
|
||||
ExtBoolNames map[int][]byte
|
||||
|
||||
// ExtNums are the extended num capabilities.
|
||||
ExtNums map[int]int
|
||||
|
||||
// ExtNumsNames is the map of extended num capabilities to their index.
|
||||
ExtNumNames map[int][]byte
|
||||
|
||||
// ExtStrings are the extended string capabilities.
|
||||
ExtStrings map[int][]byte
|
||||
|
||||
// ExtStringsNames is the map of extended string capabilities to their index.
|
||||
ExtStringNames map[int][]byte
|
||||
}
|
||||
@@ -100,75 +79,63 @@ type Terminfo struct {
|
||||
// Decode decodes the terminfo data contained in buf.
|
||||
func Decode(buf []byte) (*Terminfo, error) {
|
||||
var err error
|
||||
|
||||
// check max file length
|
||||
if len(buf) >= maxFileLength {
|
||||
return nil, ErrInvalidFileSize
|
||||
}
|
||||
|
||||
d := &decoder{
|
||||
buf: buf,
|
||||
len: len(buf),
|
||||
n: len(buf),
|
||||
}
|
||||
|
||||
// read header
|
||||
h, err := d.readInts(6, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var numWidth int
|
||||
|
||||
// check magic
|
||||
if h[fieldMagic] == magic {
|
||||
switch {
|
||||
case h[fieldMagic] == magic:
|
||||
numWidth = 16
|
||||
} else if h[fieldMagic] == magicExtended {
|
||||
case h[fieldMagic] == magicExtended:
|
||||
numWidth = 32
|
||||
} else {
|
||||
default:
|
||||
return nil, ErrInvalidMagic
|
||||
}
|
||||
|
||||
// check header
|
||||
if hasInvalidCaps(h) {
|
||||
return nil, ErrInvalidHeader
|
||||
}
|
||||
|
||||
// check remaining length
|
||||
if d.len-d.pos < capLength(h) {
|
||||
if d.n-d.pos < capLength(h) {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
|
||||
// read names
|
||||
names, err := d.readBytes(h[fieldNameSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check name is terminated properly
|
||||
i := findNull(names, 0)
|
||||
if i == -1 {
|
||||
return nil, ErrInvalidNames
|
||||
}
|
||||
names = names[:i]
|
||||
|
||||
// read bool caps
|
||||
bools, boolsM, err := d.readBools(h[fieldBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read num caps
|
||||
nums, numsM, err := d.readNums(h[fieldNumCount], numWidth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read string caps
|
||||
strs, strsM, err := d.readStrings(h[fieldStringCount], h[fieldTableSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ti := &Terminfo{
|
||||
Names: strings.Split(string(names), "|"),
|
||||
Bools: bools,
|
||||
@@ -178,57 +145,47 @@ func Decode(buf []byte) (*Terminfo, error) {
|
||||
Strings: strs,
|
||||
StringsM: strsM,
|
||||
}
|
||||
|
||||
// at the end of file, so no extended caps
|
||||
if d.pos >= d.len {
|
||||
if d.pos >= d.n {
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
// decode extended header
|
||||
eh, err := d.readInts(5, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check extended offset field
|
||||
if hasInvalidExtOffset(eh) {
|
||||
return nil, ErrInvalidExtendedHeader
|
||||
}
|
||||
|
||||
// check extended cap lengths
|
||||
if d.len-d.pos != extCapLength(eh, numWidth) {
|
||||
if d.n-d.pos != extCapLength(eh, numWidth) {
|
||||
return nil, ErrInvalidExtendedHeader
|
||||
}
|
||||
|
||||
// read extended bool caps
|
||||
ti.ExtBools, _, err = d.readBools(eh[fieldExtBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read extended num caps
|
||||
ti.ExtNums, _, err = d.readNums(eh[fieldExtNumCount], numWidth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read extended string data table indexes
|
||||
extIndexes, err := d.readInts(eh[fieldExtOffsetCount], 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read string data table
|
||||
extData, err := d.readBytes(eh[fieldExtTableSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// precautionary check that exactly at end of file
|
||||
if d.pos != d.len {
|
||||
if d.pos != d.n {
|
||||
return nil, ErrUnexpectedFileEnd
|
||||
}
|
||||
|
||||
var last int
|
||||
// read extended string caps
|
||||
ti.ExtStrings, last, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
|
||||
@@ -236,28 +193,24 @@ func Decode(buf []byte) (*Terminfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
extIndexes, extData = extIndexes[eh[fieldExtStringCount]:], extData[last:]
|
||||
|
||||
// read extended bool names
|
||||
ti.ExtBoolNames, _, err = readStrings(extIndexes, extData, eh[fieldExtBoolCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extIndexes = extIndexes[eh[fieldExtBoolCount]:]
|
||||
|
||||
// read extended num names
|
||||
ti.ExtNumNames, _, err = readStrings(extIndexes, extData, eh[fieldExtNumCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extIndexes = extIndexes[eh[fieldExtNumCount]:]
|
||||
|
||||
// read extended string names
|
||||
ti.ExtStringNames, _, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//extIndexes = extIndexes[eh[fieldExtStringCount]:]
|
||||
|
||||
// extIndexes = extIndexes[eh[fieldExtStringCount]:]
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
@@ -279,23 +232,19 @@ func Open(dir, name string) (*Terminfo, error) {
|
||||
if buf == nil {
|
||||
return nil, ErrFileNotFound
|
||||
}
|
||||
|
||||
// decode
|
||||
ti, err := Decode(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save original file name
|
||||
ti.File = filename
|
||||
|
||||
// add to cache
|
||||
termCache.Lock()
|
||||
for _, n := range ti.Names {
|
||||
termCache.db[n] = ti
|
||||
}
|
||||
termCache.Unlock()
|
||||
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
@@ -441,7 +390,6 @@ func (ti *Terminfo) Fprintf(w io.Writer, i int, v ...interface{}) {
|
||||
// them for this terminal.
|
||||
func (ti *Terminfo) Colorf(fg, bg int, str string) string {
|
||||
maxColors := int(ti.Nums[MaxColors])
|
||||
|
||||
// map bright colors to lower versions if the color table only holds 8.
|
||||
if maxColors == 8 {
|
||||
if fg > 7 && fg < 16 {
|
||||
@@ -451,7 +399,6 @@ func (ti *Terminfo) Colorf(fg, bg int, str string) string {
|
||||
bg -= 8
|
||||
}
|
||||
}
|
||||
|
||||
var s string
|
||||
if maxColors > fg && fg >= 0 {
|
||||
s += ti.Printf(SetAForeground, fg)
|
||||
@@ -480,20 +427,17 @@ func (ti *Terminfo) Goto(row, col int) string {
|
||||
// most strings don't need padding, which is good news!
|
||||
return io.WriteString(w, s)
|
||||
}
|
||||
|
||||
end := strings.Index(s, ">")
|
||||
if end == -1 {
|
||||
// unterminated... just emit bytes unadulterated.
|
||||
return io.WriteString(w, "$<"+s)
|
||||
}
|
||||
|
||||
var c int
|
||||
c, err = io.WriteString(w, s[:start])
|
||||
if err != nil {
|
||||
return n + c, err
|
||||
}
|
||||
n += c
|
||||
|
||||
s = s[start+2:]
|
||||
val := s[:end]
|
||||
s = s[end+1:]
|
||||
@@ -518,13 +462,11 @@ func (ti *Terminfo) Goto(row, col int) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
z, pad := ((baud/8)/unit)*ms, ti.Strings[PadChar]
|
||||
b := make([]byte, len(pad)*z)
|
||||
for bp := copy(b, pad); bp < len(b); bp *= 2 {
|
||||
copy(b[bp:], b[:bp])
|
||||
}
|
||||
|
||||
if (!ti.Bools[XonXoff] && baud > int(ti.Nums[PaddingBaudRate])) || mandatory {
|
||||
c, err = w.Write(b)
|
||||
if err != nil {
|
||||
@@ -533,6 +475,5 @@ func (ti *Terminfo) Goto(row, col int) string {
|
||||
n += c
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}*/
|
||||
|
||||
Vendored
+15
-6
@@ -17,6 +17,15 @@ github.com/bahlo/generic-list-go
|
||||
# github.com/buger/jsonparser v1.1.2
|
||||
## explicit; go 1.13
|
||||
github.com/buger/jsonparser
|
||||
# github.com/cli/go-gh/v2 v2.13.0
|
||||
## explicit; go 1.25.0
|
||||
github.com/cli/go-gh/v2/internal/set
|
||||
github.com/cli/go-gh/v2/internal/yamlmap
|
||||
github.com/cli/go-gh/v2/pkg/auth
|
||||
github.com/cli/go-gh/v2/pkg/config
|
||||
# github.com/cli/safeexec v1.0.1
|
||||
## explicit; go 1.15
|
||||
github.com/cli/safeexec
|
||||
# github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
## explicit
|
||||
github.com/cloudfoundry/jibber_jabber
|
||||
@@ -119,11 +128,11 @@ github.com/lucasb-eyer/go-colorful
|
||||
## explicit; go 1.12
|
||||
github.com/mailru/easyjson/buffer
|
||||
github.com/mailru/easyjson/jwriter
|
||||
# github.com/mattn/go-colorable v0.1.11
|
||||
## explicit; go 1.13
|
||||
# github.com/mattn/go-colorable v0.1.13
|
||||
## explicit; go 1.15
|
||||
github.com/mattn/go-colorable
|
||||
# github.com/mattn/go-isatty v0.0.14
|
||||
## explicit; go 1.12
|
||||
# github.com/mattn/go-isatty v0.0.20
|
||||
## explicit; go 1.15
|
||||
github.com/mattn/go-isatty
|
||||
# github.com/mgutz/str v1.2.0
|
||||
## explicit
|
||||
@@ -179,8 +188,8 @@ github.com/stretchr/testify/assert/yaml
|
||||
# github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
## explicit; go 1.18
|
||||
github.com/wk8/go-ordered-map/v2
|
||||
# github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
## explicit; go 1.15
|
||||
# github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
## explicit; go 1.19
|
||||
github.com/xo/terminfo
|
||||
# golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
## explicit; go 1.20
|
||||
|
||||
Reference in New Issue
Block a user