🔥 github package
This commit is contained in:
parent
a8aa5feb02
commit
63f4026367
8 changed files with 0 additions and 1400 deletions
290
github/client.go
290
github/client.go
|
|
@ -1,290 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/version"
|
||||
)
|
||||
|
||||
const (
|
||||
GitHubHost string = "github.com"
|
||||
OAuthAppURL string = "https://github.com/github/gh-cli"
|
||||
)
|
||||
|
||||
var userAgent = "GitHub CLI " + version.Version
|
||||
|
||||
func NewClient(h string) *Client {
|
||||
return NewClientWithHost(&Host{Host: h})
|
||||
}
|
||||
|
||||
func NewClientWithHost(host *Host) *Client {
|
||||
return &Client{Host: host}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Host *Host
|
||||
cachedClient *simpleClient
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
func (client *Client) CurrentUser() (user *User, err error) {
|
||||
api, err := client.simpleApi()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := api.Get("user")
|
||||
if err = checkStatus(200, "getting current user", res, err); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user = &User{}
|
||||
err = res.Unmarshal(user)
|
||||
return
|
||||
}
|
||||
|
||||
type AuthorizationEntry struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func isToken(api *simpleClient, password string) bool {
|
||||
api.PrepareRequest = func(req *http.Request) {
|
||||
req.Header.Set("Authorization", "token "+password)
|
||||
}
|
||||
|
||||
res, _ := api.Get("user")
|
||||
if res != nil && res.StatusCode == 200 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (client *Client) FindOrCreateToken(user, password, twoFactorCode string) (token string, err error) {
|
||||
api := client.apiClient()
|
||||
|
||||
if len(password) >= 40 && isToken(api, password) {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"scopes": []string{"repo"},
|
||||
"note_url": OAuthAppURL,
|
||||
}
|
||||
|
||||
api.PrepareRequest = func(req *http.Request) {
|
||||
req.SetBasicAuth(user, password)
|
||||
if twoFactorCode != "" {
|
||||
req.Header.Set("X-GitHub-OTP", twoFactorCode)
|
||||
}
|
||||
}
|
||||
|
||||
count := 1
|
||||
maxTries := 9
|
||||
for {
|
||||
params["note"], err = authTokenNote(count)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, postErr := api.PostJSON("authorizations", params)
|
||||
if postErr != nil {
|
||||
err = postErr
|
||||
break
|
||||
}
|
||||
|
||||
if res.StatusCode == 201 {
|
||||
auth := &AuthorizationEntry{}
|
||||
if err = res.Unmarshal(auth); err != nil {
|
||||
return
|
||||
}
|
||||
token = auth.Token
|
||||
break
|
||||
} else if res.StatusCode == 422 && count < maxTries {
|
||||
count++
|
||||
} else {
|
||||
errInfo, e := res.ErrorInfo()
|
||||
if e == nil {
|
||||
err = errInfo
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) ensureAccessToken() error {
|
||||
if client.Host.AccessToken == "" {
|
||||
host, err := CurrentConfig().PromptForHost(client.Host.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client.Host = host
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) simpleApi() (c *simpleClient, err error) {
|
||||
err = client.ensureAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if client.cachedClient != nil {
|
||||
c = client.cachedClient
|
||||
return
|
||||
}
|
||||
|
||||
c = client.apiClient()
|
||||
c.PrepareRequest = func(req *http.Request) {
|
||||
clientDomain := normalizeHost(client.Host.Host)
|
||||
if strings.HasPrefix(clientDomain, "api.github.") {
|
||||
clientDomain = strings.TrimPrefix(clientDomain, "api.")
|
||||
}
|
||||
requestHost := strings.ToLower(req.URL.Host)
|
||||
if requestHost == clientDomain || strings.HasSuffix(requestHost, "."+clientDomain) {
|
||||
req.Header.Set("Authorization", "token "+client.Host.AccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
client.cachedClient = c
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) apiClient() *simpleClient {
|
||||
unixSocket := os.ExpandEnv(client.Host.UnixSocket)
|
||||
httpClient := newHttpClient(os.Getenv("HUB_TEST_HOST"), os.Getenv("HUB_VERBOSE") != "", unixSocket)
|
||||
apiRoot := client.absolute(normalizeHost(client.Host.Host))
|
||||
if !strings.HasPrefix(apiRoot.Host, "api.github.") {
|
||||
apiRoot.Path = "/api/v3/"
|
||||
}
|
||||
|
||||
return &simpleClient{
|
||||
httpClient: httpClient,
|
||||
rootUrl: apiRoot,
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) absolute(host string) *url.URL {
|
||||
u, err := url.Parse("https://" + host + "/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if client.Host != nil && client.Host.Protocol != "" {
|
||||
u.Scheme = client.Host.Protocol
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func normalizeHost(host string) string {
|
||||
if host == "" {
|
||||
return GitHubHost
|
||||
} else if strings.EqualFold(host, GitHubHost) {
|
||||
return "api.github.com"
|
||||
} else if strings.EqualFold(host, "github.localhost") {
|
||||
return "api.github.localhost"
|
||||
} else {
|
||||
return strings.ToLower(host)
|
||||
}
|
||||
}
|
||||
|
||||
func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error %s: %s", action, err.Error())
|
||||
} else if response.StatusCode != expectedStatus {
|
||||
errInfo, err := response.ErrorInfo()
|
||||
if err == nil {
|
||||
return FormatError(action, errInfo)
|
||||
} else {
|
||||
return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func FormatError(action string, err error) (ee error) {
|
||||
switch e := err.(type) {
|
||||
default:
|
||||
ee = err
|
||||
case *errorInfo:
|
||||
statusCode := e.Response.StatusCode
|
||||
var reason string
|
||||
if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 {
|
||||
reason = strings.TrimSpace(s[1])
|
||||
}
|
||||
|
||||
errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode)
|
||||
|
||||
var errorSentences []string
|
||||
for _, err := range e.Errors {
|
||||
switch err.Code {
|
||||
case "custom":
|
||||
errorSentences = append(errorSentences, err.Message)
|
||||
case "missing_field":
|
||||
errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field))
|
||||
case "already_exists":
|
||||
errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field))
|
||||
case "invalid":
|
||||
errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field))
|
||||
case "unauthorized":
|
||||
errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field))
|
||||
}
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
if len(errorSentences) > 0 {
|
||||
errorMessage = strings.Join(errorSentences, "\n")
|
||||
} else {
|
||||
errorMessage = e.Message
|
||||
if action == "getting current user" && e.Message == "Resource not accessible by integration" {
|
||||
errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable."
|
||||
}
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage)
|
||||
}
|
||||
|
||||
ee = fmt.Errorf(errStr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func authTokenNote(num int) (string, error) {
|
||||
n := os.Getenv("USER")
|
||||
|
||||
if n == "" {
|
||||
n = os.Getenv("USERNAME")
|
||||
}
|
||||
|
||||
if n == "" {
|
||||
whoami := exec.Command("whoami")
|
||||
whoamiOut, err := whoami.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n = strings.TrimSpace(string(whoamiOut))
|
||||
}
|
||||
|
||||
h, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if num > 1 {
|
||||
return fmt.Sprintf("hub for %s@%s %d", n, h, num), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("hub for %s@%s", n, h), nil
|
||||
}
|
||||
397
github/config.go
397
github/config.go
|
|
@ -1,397 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/github/gh-cli/ui"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type yamlHost struct {
|
||||
User string `yaml:"user"`
|
||||
OAuthToken string `yaml:"oauth_token"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
UnixSocket string `yaml:"unix_socket,omitempty"`
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
Host string `toml:"host"`
|
||||
User string `toml:"user"`
|
||||
AccessToken string `toml:"access_token"`
|
||||
Protocol string `toml:"protocol"`
|
||||
UnixSocket string `toml:"unix_socket,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Hosts []*Host `toml:"hosts"`
|
||||
}
|
||||
|
||||
func (c *Config) PromptForHost(host string) (h *Host, err error) {
|
||||
token := c.DetectToken()
|
||||
tokenFromEnv := token != ""
|
||||
|
||||
if host != GitHubHost {
|
||||
if _, e := url.Parse("https://" + host); e != nil {
|
||||
err = fmt.Errorf("invalid hostname: %q", host)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h = c.Find(host)
|
||||
if h != nil {
|
||||
if h.User == "" {
|
||||
utils.Check(CheckWriteable(configsFile()))
|
||||
// User is missing from the config: this is a broken config probably
|
||||
// because it was created with an old (broken) version of hub. Let's fix
|
||||
// it now. See issue #1007 for details.
|
||||
user := c.PromptForUser(host)
|
||||
if user == "" {
|
||||
utils.Check(fmt.Errorf("missing user"))
|
||||
}
|
||||
h.User = user
|
||||
err := newConfigService().Save(configsFile(), c)
|
||||
utils.Check(err)
|
||||
}
|
||||
if tokenFromEnv {
|
||||
h.AccessToken = token
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
h = &Host{
|
||||
Host: host,
|
||||
AccessToken: token,
|
||||
Protocol: "https",
|
||||
}
|
||||
c.Hosts = append(c.Hosts, h)
|
||||
}
|
||||
|
||||
client := NewClientWithHost(h)
|
||||
|
||||
if !tokenFromEnv {
|
||||
utils.Check(CheckWriteable(configsFile()))
|
||||
err = c.authorizeClient(client, host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
userFromEnv := os.Getenv("GITHUB_USER")
|
||||
repoFromEnv := os.Getenv("GITHUB_REPOSITORY")
|
||||
if userFromEnv == "" && repoFromEnv != "" {
|
||||
repoParts := strings.SplitN(repoFromEnv, "/", 2)
|
||||
if len(repoParts) > 0 {
|
||||
userFromEnv = repoParts[0]
|
||||
}
|
||||
}
|
||||
if tokenFromEnv && userFromEnv != "" {
|
||||
h.User = userFromEnv
|
||||
} else {
|
||||
var currentUser *User
|
||||
currentUser, err = client.CurrentUser()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.User = currentUser.Login
|
||||
}
|
||||
|
||||
if !tokenFromEnv {
|
||||
err = newConfigService().Save(configsFile(), c)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) authorizeClient(client *Client, host string) (err error) {
|
||||
user := c.PromptForUser(host)
|
||||
pass := c.PromptForPassword(host, user)
|
||||
|
||||
var code, token string
|
||||
for {
|
||||
token, err = client.FindOrCreateToken(user, pass, code)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if ae, ok := err.(*errorInfo); ok && strings.HasPrefix(ae.Response.Header.Get("X-GitHub-OTP"), "required;") {
|
||||
if code != "" {
|
||||
ui.Errorln("warning: invalid two-factor code")
|
||||
}
|
||||
code = c.PromptForOTP()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
client.Host.AccessToken = token
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) DetectToken() string {
|
||||
return os.Getenv("GITHUB_TOKEN")
|
||||
}
|
||||
|
||||
func (c *Config) PromptForUser(host string) (user string) {
|
||||
user = os.Getenv("GITHUB_USER")
|
||||
if user != "" {
|
||||
return
|
||||
}
|
||||
|
||||
ui.Printf("%s username: ", host)
|
||||
user = c.scanLine()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) PromptForPassword(host, user string) (pass string) {
|
||||
pass = os.Getenv("GITHUB_PASSWORD")
|
||||
if pass != "" {
|
||||
return
|
||||
}
|
||||
|
||||
ui.Printf("%s password for %s (never stored): ", host, user)
|
||||
if ui.IsTerminal(os.Stdin) {
|
||||
if password, err := getPassword(); err == nil {
|
||||
pass = password
|
||||
}
|
||||
} else {
|
||||
pass = c.scanLine()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) PromptForOTP() string {
|
||||
fmt.Print("two-factor authentication code: ")
|
||||
return c.scanLine()
|
||||
}
|
||||
|
||||
func (c *Config) scanLine() string {
|
||||
var line string
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
}
|
||||
utils.Check(scanner.Err())
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
func getPassword() (string, error) {
|
||||
stdin := int(syscall.Stdin)
|
||||
initialTermState, err := terminal.GetState(stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
s := <-c
|
||||
terminal.Restore(stdin, initialTermState)
|
||||
switch sig := s.(type) {
|
||||
case syscall.Signal:
|
||||
if int(sig) == 2 {
|
||||
fmt.Println("^C")
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
passBytes, err := terminal.ReadPassword(stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signal.Stop(c)
|
||||
fmt.Print("\n")
|
||||
return string(passBytes), nil
|
||||
}
|
||||
|
||||
func (c *Config) Find(host string) *Host {
|
||||
for _, h := range c.Hosts {
|
||||
if h.Host == host {
|
||||
return h
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) selectHost() *Host {
|
||||
options := len(c.Hosts)
|
||||
|
||||
if options == 1 {
|
||||
return c.Hosts[0]
|
||||
}
|
||||
|
||||
prompt := "Select host:\n"
|
||||
for idx, host := range c.Hosts {
|
||||
prompt += fmt.Sprintf(" %d. %s\n", idx+1, host.Host)
|
||||
}
|
||||
prompt += fmt.Sprint("> ")
|
||||
|
||||
ui.Printf(prompt)
|
||||
index := c.scanLine()
|
||||
i, err := strconv.Atoi(index)
|
||||
if err != nil || i < 1 || i > options {
|
||||
utils.Check(fmt.Errorf("Error: must enter a number [1-%d]", options))
|
||||
}
|
||||
|
||||
return c.Hosts[i-1]
|
||||
}
|
||||
|
||||
var defaultConfigsFile string
|
||||
|
||||
func configsFile() string {
|
||||
if configFromEnv := os.Getenv("HUB_CONFIG"); configFromEnv != "" {
|
||||
return configFromEnv
|
||||
}
|
||||
if defaultConfigsFile == "" {
|
||||
var err error
|
||||
defaultConfigsFile, err = determineConfigLocation()
|
||||
utils.Check(err)
|
||||
}
|
||||
return defaultConfigsFile
|
||||
}
|
||||
|
||||
func homeConfig() (string, error) {
|
||||
if home, err := homedir.Dir(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func determineConfigLocation() (string, error) {
|
||||
var err error
|
||||
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
configDir := xdgHome
|
||||
if configDir == "" {
|
||||
if configDir, err = homeConfig(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
xdgDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if xdgDirs == "" {
|
||||
xdgDirs = "/etc/xdg"
|
||||
}
|
||||
searchDirs := append([]string{configDir}, strings.Split(xdgDirs, ":")...)
|
||||
|
||||
for _, dir := range searchDirs {
|
||||
filename := filepath.Join(dir, "hub")
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return filename, nil
|
||||
}
|
||||
}
|
||||
|
||||
configFile := filepath.Join(configDir, "hub")
|
||||
|
||||
if configDir == xdgHome {
|
||||
if homeDir, _ := homeConfig(); homeDir != "" {
|
||||
legacyConfig := filepath.Join(homeDir, "hub")
|
||||
if _, err = os.Stat(legacyConfig); err == nil {
|
||||
ui.Errorf("Notice: config file found but not respected at: %s\n", legacyConfig)
|
||||
ui.Errorf("You might want to move it to `%s' to avoid re-authenticating.\n", configFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
var currentConfig *Config
|
||||
var configLoadedFrom = ""
|
||||
|
||||
func CurrentConfig() *Config {
|
||||
filename := configsFile()
|
||||
if configLoadedFrom != filename {
|
||||
currentConfig = &Config{}
|
||||
newConfigService().Load(filename, currentConfig)
|
||||
configLoadedFrom = filename
|
||||
}
|
||||
|
||||
return currentConfig
|
||||
}
|
||||
|
||||
func (c *Config) DefaultHost() (host *Host, err error) {
|
||||
if GitHubHostEnv != "" {
|
||||
host, err = c.PromptForHost(GitHubHostEnv)
|
||||
} else if len(c.Hosts) > 0 {
|
||||
host = c.selectHost()
|
||||
// HACK: forces host to inherit GITHUB_TOKEN if applicable
|
||||
host, err = c.PromptForHost(host.Host)
|
||||
} else {
|
||||
host, err = c.PromptForHost(DefaultGitHubHost())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CheckWriteable checks if config file is writeable. This should
|
||||
// be called before asking for credentials and only if current
|
||||
// operation needs to update the file. See issue #1314 for details.
|
||||
func CheckWriteable(filename string) error {
|
||||
// Check if file exists already. if it doesn't, we will delete it after
|
||||
// checking for writeabilty
|
||||
fileExistsAlready := false
|
||||
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
fileExistsAlready = true
|
||||
}
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0771)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Close()
|
||||
|
||||
if !fileExistsAlready {
|
||||
err := os.Remove(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Public for testing purpose
|
||||
func CreateTestConfigs(user, token string) *Config {
|
||||
f, _ := ioutil.TempFile("", "test-config")
|
||||
os.Setenv("HUB_CONFIG", f.Name())
|
||||
|
||||
host := &Host{
|
||||
User: "jingweno",
|
||||
AccessToken: "123",
|
||||
Host: GitHubHost,
|
||||
}
|
||||
|
||||
c := &Config{Hosts: []*Host{host}}
|
||||
err := newConfigService().Save(f.Name(), c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type configDecoder interface {
|
||||
Decode(r io.Reader, c *Config) error
|
||||
}
|
||||
|
||||
type tomlConfigDecoder struct {
|
||||
}
|
||||
|
||||
func (t *tomlConfigDecoder) Decode(r io.Reader, c *Config) error {
|
||||
_, err := toml.DecodeReader(r, c)
|
||||
return err
|
||||
}
|
||||
|
||||
type yamlConfigDecoder struct {
|
||||
}
|
||||
|
||||
func (y *yamlConfigDecoder) Decode(r io.Reader, c *Config) error {
|
||||
d, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
yc := yaml.MapSlice{}
|
||||
err = yaml.Unmarshal(d, &yc)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, hostEntry := range yc {
|
||||
v := hostEntry.Value.([]interface{})
|
||||
if len(v) < 1 {
|
||||
continue
|
||||
}
|
||||
host := &Host{Host: hostEntry.Key.(string)}
|
||||
for _, prop := range v[0].(yaml.MapSlice) {
|
||||
switch prop.Key.(string) {
|
||||
case "user":
|
||||
host.User = prop.Value.(string)
|
||||
case "oauth_token":
|
||||
host.AccessToken = prop.Value.(string)
|
||||
case "protocol":
|
||||
host.Protocol = prop.Value.(string)
|
||||
case "unix_socket":
|
||||
host.UnixSocket = prop.Value.(string)
|
||||
}
|
||||
}
|
||||
c.Hosts = append(c.Hosts, host)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type configEncoder interface {
|
||||
Encode(w io.Writer, c *Config) error
|
||||
}
|
||||
|
||||
type tomlConfigEncoder struct {
|
||||
}
|
||||
|
||||
func (t *tomlConfigEncoder) Encode(w io.Writer, c *Config) error {
|
||||
enc := toml.NewEncoder(w)
|
||||
return enc.Encode(c)
|
||||
}
|
||||
|
||||
type yamlConfigEncoder struct {
|
||||
}
|
||||
|
||||
func (y *yamlConfigEncoder) Encode(w io.Writer, c *Config) error {
|
||||
yc := yaml.MapSlice{}
|
||||
for _, h := range c.Hosts {
|
||||
yc = append(yc, yaml.MapItem{
|
||||
Key: h.Host,
|
||||
Value: []yamlHost{
|
||||
{
|
||||
User: h.User,
|
||||
OAuthToken: h.AccessToken,
|
||||
Protocol: h.Protocol,
|
||||
UnixSocket: h.UnixSocket,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
d, err := yaml.Marshal(yc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := w.Write(d)
|
||||
if err == nil && n < len(d) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func newConfigService() *configService {
|
||||
return &configService{
|
||||
Encoder: &yamlConfigEncoder{},
|
||||
Decoder: &yamlConfigDecoder{},
|
||||
}
|
||||
}
|
||||
|
||||
type configService struct {
|
||||
Encoder configEncoder
|
||||
Decoder configDecoder
|
||||
}
|
||||
|
||||
func (s *configService) Save(filename string, c *Config) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0771)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
return s.Encoder.Encode(w, c)
|
||||
}
|
||||
|
||||
func (s *configService) Load(filename string, c *Config) error {
|
||||
r, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return s.Decoder.Decode(r, c)
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/git"
|
||||
)
|
||||
|
||||
var (
|
||||
GitHubHostEnv = os.Getenv("GITHUB_HOST")
|
||||
cachedHosts []string
|
||||
)
|
||||
|
||||
type GithubHostError struct {
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func (e *GithubHostError) Error() string {
|
||||
return fmt.Sprintf("Invalid GitHub URL: %s", e.url)
|
||||
}
|
||||
|
||||
func KnownGitHubHostsInclude(host string) bool {
|
||||
for _, hh := range knownGitHubHosts() {
|
||||
if hh == host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func knownGitHubHosts() []string {
|
||||
if cachedHosts != nil {
|
||||
return cachedHosts
|
||||
}
|
||||
|
||||
hosts := []string{}
|
||||
defaultHost := DefaultGitHubHost()
|
||||
hosts = append(hosts, defaultHost)
|
||||
hosts = append(hosts, "ssh.github.com")
|
||||
|
||||
ghHosts, _ := git.ConfigAll("hub.host")
|
||||
for _, ghHost := range ghHosts {
|
||||
ghHost = strings.TrimSpace(ghHost)
|
||||
if ghHost != "" {
|
||||
hosts = append(hosts, ghHost)
|
||||
}
|
||||
}
|
||||
|
||||
cachedHosts = hosts
|
||||
return hosts
|
||||
}
|
||||
|
||||
func DefaultGitHubHost() string {
|
||||
defaultHost := GitHubHostEnv
|
||||
if defaultHost == "" {
|
||||
defaultHost = GitHubHost
|
||||
}
|
||||
|
||||
return defaultHost
|
||||
}
|
||||
490
github/http.go
490
github/http.go
|
|
@ -1,490 +0,0 @@
|
|||
package github
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/gh-cli/ui"
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
const apiPayloadVersion = "application/vnd.github.v3+json;charset=utf-8"
|
||||
const patchMediaType = "application/vnd.github.v3.patch;charset=utf-8"
|
||||
const textMediaType = "text/plain;charset=utf-8"
|
||||
const checksType = "application/vnd.github.antiope-preview+json;charset=utf-8"
|
||||
const draftsType = "application/vnd.github.shadow-cat-preview+json;charset=utf-8"
|
||||
const cacheVersion = 2
|
||||
|
||||
var inspectHeaders = []string{
|
||||
"Authorization",
|
||||
"X-GitHub-OTP",
|
||||
"Location",
|
||||
"Link",
|
||||
"Accept",
|
||||
}
|
||||
|
||||
type verboseTransport struct {
|
||||
Transport *http.Transport
|
||||
Verbose bool
|
||||
OverrideURL *url.URL
|
||||
Out io.Writer
|
||||
Colorized bool
|
||||
}
|
||||
|
||||
func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
if t.Verbose {
|
||||
t.dumpRequest(req)
|
||||
}
|
||||
|
||||
if t.OverrideURL != nil {
|
||||
port := "80"
|
||||
if s := strings.Split(req.URL.Host, ":"); len(s) > 1 {
|
||||
port = s[1]
|
||||
}
|
||||
|
||||
req = cloneRequest(req)
|
||||
req.Header.Set("X-Original-Scheme", req.URL.Scheme)
|
||||
req.Header.Set("X-Original-Port", port)
|
||||
req.Host = req.URL.Host
|
||||
req.URL.Scheme = t.OverrideURL.Scheme
|
||||
req.URL.Host = t.OverrideURL.Host
|
||||
}
|
||||
|
||||
resp, err = t.Transport.RoundTrip(req)
|
||||
|
||||
if err == nil && t.Verbose {
|
||||
t.dumpResponse(resp)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *verboseTransport) dumpRequest(req *http.Request) {
|
||||
info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI())
|
||||
t.verbosePrintln(info)
|
||||
t.dumpHeaders(req.Header, ">")
|
||||
body := t.dumpBody(req.Body)
|
||||
if body != nil {
|
||||
// reset body since it's been read
|
||||
req.Body = body
|
||||
}
|
||||
}
|
||||
|
||||
func (t *verboseTransport) dumpResponse(resp *http.Response) {
|
||||
info := fmt.Sprintf("< HTTP %d", resp.StatusCode)
|
||||
t.verbosePrintln(info)
|
||||
t.dumpHeaders(resp.Header, "<")
|
||||
body := t.dumpBody(resp.Body)
|
||||
if body != nil {
|
||||
// reset body since it's been read
|
||||
resp.Body = body
|
||||
}
|
||||
}
|
||||
|
||||
func (t *verboseTransport) dumpHeaders(header http.Header, indent string) {
|
||||
for _, listed := range inspectHeaders {
|
||||
for name, vv := range header {
|
||||
if !strings.EqualFold(name, listed) {
|
||||
continue
|
||||
}
|
||||
for _, v := range vv {
|
||||
if v != "" {
|
||||
r := regexp.MustCompile("(?i)^(basic|token) (.+)")
|
||||
if r.MatchString(v) {
|
||||
v = r.ReplaceAllString(v, "$1 [REDACTED]")
|
||||
}
|
||||
|
||||
info := fmt.Sprintf("%s %s: %s", indent, name, v)
|
||||
t.verbosePrintln(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *verboseTransport) dumpBody(body io.ReadCloser) io.ReadCloser {
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := io.Copy(buf, body)
|
||||
utils.Check(err)
|
||||
|
||||
if buf.Len() > 0 {
|
||||
t.verbosePrintln(buf.String())
|
||||
}
|
||||
|
||||
return ioutil.NopCloser(buf)
|
||||
}
|
||||
|
||||
func (t *verboseTransport) verbosePrintln(msg string) {
|
||||
if t.Colorized {
|
||||
msg = fmt.Sprintf("\033[36m%s\033[0m", msg)
|
||||
}
|
||||
|
||||
fmt.Fprintln(t.Out, msg)
|
||||
}
|
||||
|
||||
func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Client {
|
||||
var testURL *url.URL
|
||||
if testHost != "" {
|
||||
testURL, _ = url.Parse(testHost)
|
||||
}
|
||||
var httpTransport *http.Transport
|
||||
if unixSocket != "" {
|
||||
dialFunc := func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial("unix", unixSocket)
|
||||
}
|
||||
dialContext := func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", unixSocket)
|
||||
}
|
||||
httpTransport = &http.Transport{
|
||||
DialContext: dialContext,
|
||||
DialTLS: dialFunc,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
} else {
|
||||
httpTransport = &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
tr := &verboseTransport{
|
||||
Transport: httpTransport,
|
||||
Verbose: verbose,
|
||||
OverrideURL: testURL,
|
||||
Out: ui.Stderr,
|
||||
Colorized: ui.IsTerminal(os.Stderr),
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneRequest(req *http.Request) *http.Request {
|
||||
dup := new(http.Request)
|
||||
*dup = *req
|
||||
dup.URL, _ = url.Parse(req.URL.String())
|
||||
dup.Header = make(http.Header)
|
||||
for k, s := range req.Header {
|
||||
dup.Header[k] = s
|
||||
}
|
||||
return dup
|
||||
}
|
||||
|
||||
type simpleClient struct {
|
||||
httpClient *http.Client
|
||||
rootUrl *url.URL
|
||||
PrepareRequest func(*http.Request)
|
||||
CacheTTL int
|
||||
}
|
||||
|
||||
func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (*simpleResponse, error) {
|
||||
url, err := url.Parse(path)
|
||||
if err == nil {
|
||||
url = c.rootUrl.ResolveReference(url)
|
||||
return c.performRequestUrl(method, url, body, configure)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *simpleClient) performRequestUrl(method string, url *url.URL, body io.Reader, configure func(*http.Request)) (res *simpleResponse, err error) {
|
||||
req, err := http.NewRequest(method, url.String(), body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.PrepareRequest != nil {
|
||||
c.PrepareRequest(req)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Accept", apiPayloadVersion)
|
||||
|
||||
if configure != nil {
|
||||
configure(req)
|
||||
}
|
||||
|
||||
key := cacheKey(req)
|
||||
if cachedResponse := c.cacheRead(key, req); cachedResponse != nil {
|
||||
res = &simpleResponse{cachedResponse}
|
||||
return
|
||||
}
|
||||
|
||||
httpResponse, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.cacheWrite(key, httpResponse)
|
||||
res = &simpleResponse{httpResponse}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func isGraphQL(req *http.Request) bool {
|
||||
return req.URL.Path == "/graphql"
|
||||
}
|
||||
|
||||
func canCache(req *http.Request) bool {
|
||||
return strings.EqualFold(req.Method, "GET") || isGraphQL(req)
|
||||
}
|
||||
|
||||
func (c *simpleClient) cacheRead(key string, req *http.Request) (res *http.Response) {
|
||||
if c.CacheTTL > 0 && canCache(req) {
|
||||
f := cacheFile(key)
|
||||
cacheInfo, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if time.Since(cacheInfo.ModTime()).Seconds() > float64(c.CacheTTL) {
|
||||
return
|
||||
}
|
||||
cf, err := os.Open(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cf.Close()
|
||||
|
||||
cb, err := ioutil.ReadAll(cf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(string(cb), "\r\n\r\n", 2)
|
||||
if len(parts) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
res = &http.Response{
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(parts[1])),
|
||||
Header: http.Header{},
|
||||
}
|
||||
headerLines := strings.Split(parts[0], "\r\n")
|
||||
if len(headerLines) < 1 {
|
||||
return
|
||||
}
|
||||
if proto := strings.SplitN(headerLines[0], " ", 3); len(proto) >= 3 {
|
||||
res.Proto = proto[0]
|
||||
res.Status = fmt.Sprintf("%s %s", proto[1], proto[2])
|
||||
if code, _ := strconv.Atoi(proto[1]); code > 0 {
|
||||
res.StatusCode = code
|
||||
}
|
||||
}
|
||||
for _, line := range headerLines[1:] {
|
||||
kv := strings.SplitN(line, ":", 2)
|
||||
if len(kv) >= 2 {
|
||||
res.Header.Add(kv[0], strings.TrimLeft(kv[1], " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *simpleClient) cacheWrite(key string, res *http.Response) {
|
||||
if c.CacheTTL > 0 && canCache(res.Request) && res.StatusCode < 500 && res.StatusCode != 403 {
|
||||
bodyCopy := &bytes.Buffer{}
|
||||
bodyReplacement := readCloserCallback{
|
||||
Reader: io.TeeReader(res.Body, bodyCopy),
|
||||
Closer: res.Body,
|
||||
Callback: func() {
|
||||
f := cacheFile(key)
|
||||
err := os.MkdirAll(filepath.Dir(f), 0771)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cf, err := os.OpenFile(f, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cf.Close()
|
||||
fmt.Fprintf(cf, "%s %s\r\n", res.Proto, res.Status)
|
||||
res.Header.Write(cf)
|
||||
fmt.Fprintf(cf, "\r\n")
|
||||
io.Copy(cf, bodyCopy)
|
||||
},
|
||||
}
|
||||
res.Body = &bodyReplacement
|
||||
}
|
||||
}
|
||||
|
||||
type readCloserCallback struct {
|
||||
Callback func()
|
||||
Closer io.Closer
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (rc *readCloserCallback) Close() error {
|
||||
err := rc.Closer.Close()
|
||||
if err == nil {
|
||||
rc.Callback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cacheKey(req *http.Request) string {
|
||||
path := strings.Replace(req.URL.EscapedPath(), "/", "-", -1)
|
||||
if len(path) > 1 {
|
||||
path = strings.TrimPrefix(path, "-")
|
||||
}
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
hash := md5.New()
|
||||
fmt.Fprintf(hash, "%d:", cacheVersion)
|
||||
io.WriteString(hash, req.Header.Get("Accept"))
|
||||
io.WriteString(hash, req.Header.Get("Authorization"))
|
||||
queryParts := strings.Split(req.URL.RawQuery, "&")
|
||||
sort.Strings(queryParts)
|
||||
for _, q := range queryParts {
|
||||
fmt.Fprintf(hash, "%s&", q)
|
||||
}
|
||||
if isGraphQL(req) && req.Body != nil {
|
||||
if b, err := ioutil.ReadAll(req.Body); err == nil {
|
||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||
hash.Write(b)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s/%s_%x", host, path, hash.Sum(nil))
|
||||
}
|
||||
|
||||
func cacheFile(key string) string {
|
||||
return path.Join(os.TempDir(), "hub", "api", key)
|
||||
}
|
||||
|
||||
func (c *simpleClient) jsonRequest(method, path string, body interface{}, configure func(*http.Request)) (*simpleResponse, error) {
|
||||
json, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := bytes.NewBuffer(json)
|
||||
|
||||
return c.performRequest(method, path, buf, func(req *http.Request) {
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
if configure != nil {
|
||||
configure(req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *simpleClient) Get(path string) (*simpleResponse, error) {
|
||||
return c.performRequest("GET", path, nil, nil)
|
||||
}
|
||||
|
||||
func (c *simpleClient) GetFile(path string, mimeType string) (*simpleResponse, error) {
|
||||
return c.performRequest("GET", path, nil, func(req *http.Request) {
|
||||
req.Header.Set("Accept", mimeType)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *simpleClient) PostJSON(path string, payload interface{}) (*simpleResponse, error) {
|
||||
return c.jsonRequest("POST", path, payload, nil)
|
||||
}
|
||||
|
||||
func (c *simpleClient) PostJSONPreview(path string, payload interface{}, mimeType string) (*simpleResponse, error) {
|
||||
return c.jsonRequest("POST", path, payload, func(req *http.Request) {
|
||||
req.Header.Set("Accept", mimeType)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) {
|
||||
return c.jsonRequest("PATCH", path, payload, nil)
|
||||
}
|
||||
|
||||
type simpleResponse struct {
|
||||
*http.Response
|
||||
}
|
||||
|
||||
type errorInfo struct {
|
||||
Message string `json:"message"`
|
||||
Errors []fieldError `json:"errors"`
|
||||
Response *http.Response
|
||||
}
|
||||
type errorInfoSimple struct {
|
||||
Message string `json:"message"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
type fieldError struct {
|
||||
Resource string `json:"resource"`
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
func (e *errorInfo) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (res *simpleResponse) Unmarshal(dest interface{}) (err error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return json.Unmarshal(body, dest)
|
||||
}
|
||||
|
||||
func (res *simpleResponse) ErrorInfo() (msg *errorInfo, err error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg = &errorInfo{}
|
||||
err = json.Unmarshal(body, msg)
|
||||
if err != nil {
|
||||
msgSimple := &errorInfoSimple{}
|
||||
if err = json.Unmarshal(body, msgSimple); err == nil {
|
||||
msg.Message = msgSimple.Message
|
||||
for _, errMsg := range msgSimple.Errors {
|
||||
msg.Errors = append(msg.Errors, fieldError{
|
||||
Code: "custom",
|
||||
Message: errMsg,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
msg.Response = res.Response
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (res *simpleResponse) Link(name string) string {
|
||||
linkVal := res.Header.Get("Link")
|
||||
re := regexp.MustCompile(`<([^>]+)>; rel="([^"]+)"`)
|
||||
for _, match := range re.FindAllStringSubmatch(linkVal, -1) {
|
||||
if match[2] == name {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/github"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -20,8 +19,6 @@ type TempGitRepo struct {
|
|||
}
|
||||
|
||||
func UseTempGitRepo() *TempGitRepo {
|
||||
github.CreateTestConfigs("mario", "i-love-peach")
|
||||
|
||||
pwd, _ := os.Getwd()
|
||||
oldEnv := make(map[string]string)
|
||||
overrideEnv := func(name, value string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue