new config infrastructure
- adds config get and config set commands - supports arbitrary k/v strings set at top and host level - supports writing an updated config, preserving comments - supports mostly lazy evaluation of yaml
This commit is contained in:
parent
82bd7b97cf
commit
a325db3051
15 changed files with 858 additions and 150 deletions
111
command/config.go
Normal file
111
command/config.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(configCmd)
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
|
||||
configGetCmd.Flags().StringP("host", "h", "", "Get per-host setting")
|
||||
configSetCmd.Flags().StringP("host", "h", "", "Set per-host setting")
|
||||
|
||||
}
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Set and get gh settings",
|
||||
Long: `Get and set key/value strings. They can be optionally scoped to a particular host.
|
||||
|
||||
Current respected settings:
|
||||
- git_protocol: https or ssh. Default is https.
|
||||
- editor: if unset, defaults to environment variables.
|
||||
`,
|
||||
}
|
||||
|
||||
var configGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Prints the value of a given configuration key.",
|
||||
Long: `Get the value for a given configuration key, optionally scoping by hostname.
|
||||
|
||||
Examples:
|
||||
gh config get git_protocol
|
||||
https
|
||||
gh config get --host example.com
|
||||
ssh
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: configGet,
|
||||
}
|
||||
|
||||
var configSetCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Updates configuration with the value of a given key.",
|
||||
Long: `Update the configuration by setting a key to a value, optionally scoping by hostname.
|
||||
|
||||
Examples:
|
||||
gh config set editor vim
|
||||
gh config set --host example.com editor eclipse
|
||||
`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: configSet,
|
||||
}
|
||||
|
||||
func configGet(cmd *cobra.Command, args []string) error {
|
||||
key := args[0]
|
||||
hostname, err := cmd.Flags().GetString("host")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := cfg.Get(hostname, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val != "" {
|
||||
out := colorableOut(cmd)
|
||||
fmt.Fprintf(out, "%s\n", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSet(cmd *cobra.Command, args []string) error {
|
||||
key := args[0]
|
||||
value := args[1]
|
||||
|
||||
hostname, err := cmd.Flags().GetString("host")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cfg.Set(hostname, key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set %q to %q: %s", key, value, err)
|
||||
}
|
||||
|
||||
err = cfg.Write()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config to disk: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
171
command/config_test.go
Normal file
171
command/config_test.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
)
|
||||
|
||||
func TestConfigGet(t *testing.T) {
|
||||
cfg := `---
|
||||
hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
editor: ed
|
||||
`
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
|
||||
output, err := RunCommand(configGetCmd, "config get editor")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get editor`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "ed\n")
|
||||
}
|
||||
|
||||
func TestConfigGet_default(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
output, err := RunCommand(configGetCmd, "config get git_protocol")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get git_protocol`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https\n")
|
||||
}
|
||||
|
||||
func TestConfigGet_not_found(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
|
||||
output, err := RunCommand(configGetCmd, "config get missing")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get missing`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
}
|
||||
|
||||
func TestConfigSet(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
defer context.StubWriteConfig(buf)()
|
||||
output, err := RunCommand(configSetCmd, "config set editor ed")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config set editor ed`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
expected := `hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: 1234567890
|
||||
editor: ed
|
||||
`
|
||||
|
||||
eq(t, buf.String(), expected)
|
||||
}
|
||||
|
||||
func TestConfigSet_update(t *testing.T) {
|
||||
cfg := `---
|
||||
hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
editor: ed
|
||||
`
|
||||
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
defer context.StubWriteConfig(buf)()
|
||||
|
||||
output, err := RunCommand(configSetCmd, "config set editor vim")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get editor`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
expected := `hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
editor: vim
|
||||
`
|
||||
eq(t, buf.String(), expected)
|
||||
}
|
||||
|
||||
func TestConfigGetHost(t *testing.T) {
|
||||
cfg := `---
|
||||
hosts:
|
||||
github.com:
|
||||
git_protocol: ssh
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
editor: ed
|
||||
git_protocol: https
|
||||
`
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
|
||||
output, err := RunCommand(configGetCmd, "config get -hgithub.com git_protocol")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get editor`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "ssh\n")
|
||||
}
|
||||
|
||||
func TestConfigSetHost(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
defer context.StubWriteConfig(buf)()
|
||||
output, err := RunCommand(configSetCmd, "config set -hgithub.com git_protocol ssh")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config set editor ed`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
expected := `hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: 1234567890
|
||||
git_protocol: ssh
|
||||
`
|
||||
|
||||
eq(t, buf.String(), expected)
|
||||
}
|
||||
|
||||
func TestConfigSetHost_update(t *testing.T) {
|
||||
cfg := `---
|
||||
hosts:
|
||||
github.com:
|
||||
git_protocol: ssh
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
`
|
||||
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
defer context.StubWriteConfig(buf)()
|
||||
|
||||
output, err := RunCommand(configSetCmd, "config set -hgithub.com git_protocol https")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `config get editor`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
expected := `hosts:
|
||||
github.com:
|
||||
git_protocol: https
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
`
|
||||
eq(t, buf.String(), expected)
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import (
|
|||
)
|
||||
|
||||
func TestIssueStatus(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ func TestIssueStatus(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueStatus_blankSlate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ Issues opened by you
|
|||
}
|
||||
|
||||
func TestIssueStatus_disabledIssues(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestIssueStatus_disabledIssues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueList(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ Showing 3 of 3 issues in OWNER/REPO
|
|||
}
|
||||
|
||||
func TestIssueList_withFlags(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ No issues match your search in OWNER/REPO
|
|||
}
|
||||
|
||||
func TestIssueList_nullAssigneeLabels(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ func TestIssueList_nullAssigneeLabels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueList_disabledIssues(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ func TestIssueList_disabledIssues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueView_web(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ func TestIssueView_web(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueView_web_numberArgWithHash(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -342,7 +342,7 @@ func TestIssueView_Preview(t *testing.T) {
|
|||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", tc.ownerRepo)
|
||||
initBlankContext("", "OWNER/REPO", tc.ownerRepo)
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ func TestIssueView_Preview(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueView_web_notFound(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -390,7 +390,7 @@ func TestIssueView_web_notFound(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueView_disabledIssues(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -408,7 +408,7 @@ func TestIssueView_disabledIssues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueView_web_urlArg(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -441,7 +441,7 @@ func TestIssueView_web_urlArg(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueCreate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -482,7 +482,7 @@ func TestIssueCreate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueCreate_disabledIssues(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -500,7 +500,7 @@ func TestIssueCreate_disabledIssues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueCreate_web(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -526,7 +526,7 @@ func TestIssueCreate_web(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIssueCreate_webTitleBody(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func TestPRCreate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -66,7 +66,7 @@ func TestPRCreate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_withForking(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponseWithPermission("OWNER", "REPO", "READ")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -109,7 +109,7 @@ func TestPRCreate_withForking(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_alreadyExists(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -142,7 +142,7 @@ func TestPRCreate_alreadyExists(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -174,7 +174,7 @@ func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_web(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -205,7 +205,7 @@ func TestPRCreate_web(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
|
@ -330,7 +330,7 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "cool_bug-fixes")
|
||||
initBlankContext("", "OWNER/REPO", "cool_bug-fixes")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -406,7 +406,7 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -483,7 +483,7 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_survey_autofill(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -541,7 +541,7 @@ func TestPRCreate_survey_autofill(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_defaults_error_autofill(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -559,7 +559,7 @@ func TestPRCreate_defaults_error_autofill(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_defaults_error_web(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -577,7 +577,7 @@ func TestPRCreate_defaults_error_web(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRCreate_defaults_error_interactive(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "feature")
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func RunCommand(cmd *cobra.Command, args string) (*cmdOut, error) {
|
|||
}
|
||||
|
||||
func TestPRStatus(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func TestPRStatus(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRStatus_fork(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubForkedRepoResponse("OWNER/REPO", "PARENT/REPO")
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ branch.blueberries.merge refs/heads/blueberries`)}
|
|||
}
|
||||
|
||||
func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRStatus_currentBranch_Closed(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRStatus_currentBranch_Merged(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRStatus_blankSlate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ Requesting a code review from you
|
|||
}
|
||||
|
||||
func TestPRList(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ Showing 3 of 3 pull requests in OWNER/REPO
|
|||
}
|
||||
|
||||
func TestPRList_filtering(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ No pull requests match your search in OWNER/REPO
|
|||
}
|
||||
|
||||
func TestPRList_filteringRemoveDuplicate(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -341,7 +341,7 @@ func TestPRList_filteringRemoveDuplicate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRList_filteringClosed(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -365,7 +365,7 @@ func TestPRList_filteringClosed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRList_filteringAssignee(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -389,7 +389,7 @@ func TestPRList_filteringAssignee(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRList_filteringAssigneeLabels(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -519,7 +519,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", tc.ownerRepo)
|
||||
initBlankContext("", "OWNER/REPO", tc.ownerRepo)
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -540,7 +540,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_currentBranch(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -578,7 +578,7 @@ func TestPRView_web_currentBranch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_noResultsForBranch(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "blueberries")
|
||||
initBlankContext("", "OWNER/REPO", "blueberries")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -609,7 +609,7 @@ func TestPRView_web_noResultsForBranch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_numberArg(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -641,7 +641,7 @@ func TestPRView_web_numberArg(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_numberArgWithHash(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -673,7 +673,7 @@ func TestPRView_web_numberArgWithHash(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_urlArg(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -704,7 +704,7 @@ func TestPRView_web_urlArg(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_branchArg(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
@ -738,7 +738,7 @@ func TestPRView_web_branchArg(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPRView_web_branchWithOwnerArg(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func stubSince(d time.Duration) func() {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
|
@ -134,7 +134,7 @@ func TestRepoFork_outside(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent_yes(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
|
@ -261,7 +261,7 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_yes(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
|
@ -303,7 +303,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_no(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
|
@ -678,7 +678,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoView_web(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -773,7 +773,7 @@ func TestRepoView_web_fullURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoView(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -801,7 +801,7 @@ func TestRepoView(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoView_nonmarkdown_readme(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
|
|
@ -828,7 +828,7 @@ func TestRepoView_nonmarkdown_readme(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoView_blanks(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString("{}"))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// TODO these are sprinkled across command, context, and ghrepo
|
||||
const defaultHostname = "github.com"
|
||||
|
||||
// Version is dynamically set by the toolchain or overridden by the Makefile.
|
||||
var Version = "DEV"
|
||||
|
||||
|
|
@ -107,7 +110,9 @@ func BasicClient() (*api.Client, error) {
|
|||
}
|
||||
opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)))
|
||||
if c, err := context.ParseDefaultConfig(); err == nil {
|
||||
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", c.Token)))
|
||||
if token, err := c.Get(defaultHostname, "oauth_token"); err == nil {
|
||||
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
|
||||
}
|
||||
}
|
||||
return api.NewClient(opts...), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ import (
|
|||
"github.com/cli/cli/context"
|
||||
)
|
||||
|
||||
const defaultTestConfig = `hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: 1234567890
|
||||
`
|
||||
|
||||
type askStubber struct {
|
||||
Asks [][]*survey.Question
|
||||
Count int
|
||||
|
|
@ -63,7 +69,7 @@ func (as *askStubber) Stub(stubbedQuestions []*QuestionStub) {
|
|||
as.Stubs = append(as.Stubs, stubbedQuestions)
|
||||
}
|
||||
|
||||
func initBlankContext(repo, branch string) {
|
||||
func initBlankContext(cfg, repo, branch string) {
|
||||
initContext = func() context.Context {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBaseRepo(repo)
|
||||
|
|
@ -71,6 +77,15 @@ func initBlankContext(repo, branch string) {
|
|||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
|
||||
if cfg == "" {
|
||||
cfg = defaultTestConfig
|
||||
}
|
||||
|
||||
// NOTE we are not restoring the original readConfig; we never want to touch the config file on
|
||||
// disk during tests.
|
||||
context.StubConfig(cfg)
|
||||
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@ type blankContext struct {
|
|||
remotes Remotes
|
||||
}
|
||||
|
||||
func (c *blankContext) Config() (Config, error) {
|
||||
config, err := ParseConfig("boom.txt")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to parse config during tests. did you remember to stub? error: %s", err))
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *blankContext) AuthToken() (string, error) {
|
||||
return c.authToken, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,57 +10,159 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const defaultHostname = "github.com"
|
||||
|
||||
type configEntry struct {
|
||||
User string
|
||||
Token string `yaml:"oauth_token"`
|
||||
}
|
||||
|
||||
func parseOrSetupConfigFile(fn string) (*configEntry, error) {
|
||||
entry, err := parseConfigFile(fn)
|
||||
func parseOrSetupConfigFile(fn string) (Config, error) {
|
||||
config, err := ParseConfig(fn)
|
||||
if err != nil && errors.Is(err, os.ErrNotExist) {
|
||||
return setupConfigFile(fn)
|
||||
}
|
||||
return entry, err
|
||||
return config, err
|
||||
}
|
||||
|
||||
func parseConfigFile(fn string) (*configEntry, error) {
|
||||
func ParseDefaultConfig() (Config, error) {
|
||||
return ParseConfig(configFile())
|
||||
}
|
||||
|
||||
var ReadConfigFile = func(fn string) ([]byte, error) {
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return parseConfig(f)
|
||||
}
|
||||
|
||||
// ParseDefaultConfig reads the configuration file
|
||||
func ParseDefaultConfig() (*configEntry, error) {
|
||||
return parseConfigFile(configFile())
|
||||
}
|
||||
|
||||
func parseConfig(r io.Reader) (*configEntry, error) {
|
||||
data, err := ioutil.ReadAll(r)
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config yaml.Node
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var WriteConfigFile = func(fn string, data []byte) error {
|
||||
cfgFile, err := os.OpenFile(configFile(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) // cargo coded from setup
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if len(config.Content) < 1 {
|
||||
return nil, fmt.Errorf("malformed config")
|
||||
defer cfgFile.Close()
|
||||
|
||||
n, err := cfgFile.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
for i := 0; i < len(config.Content[0].Content)-1; i = i + 2 {
|
||||
if config.Content[0].Content[i].Value == defaultHostname {
|
||||
var entries []configEntry
|
||||
err = config.Content[0].Content[i+1].Decode(&entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &entries[0], nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var BackupConfigFile = func(fn string) error {
|
||||
return os.Rename(fn, fn+".bak")
|
||||
}
|
||||
|
||||
func parseConfigFile(fn string) ([]byte, *yaml.Node, error) {
|
||||
data, err := ReadConfigFile(fn)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root yaml.Node
|
||||
err = yaml.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return data, nil, err
|
||||
}
|
||||
if len(root.Content) < 1 {
|
||||
return data, &root, fmt.Errorf("malformed config")
|
||||
}
|
||||
if root.Content[0].Kind != yaml.MappingNode {
|
||||
return data, &root, fmt.Errorf("expected a top level map")
|
||||
}
|
||||
|
||||
return data, &root, nil
|
||||
}
|
||||
|
||||
func isLegacy(root *yaml.Node) bool {
|
||||
for _, v := range root.Content[0].Content {
|
||||
if v.Value == "hosts" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not find config entry for %q", defaultHostname)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func migrateConfig(fn string, root *yaml.Node) error {
|
||||
// TODO i'm sorry
|
||||
newConfigData := map[string]map[string]map[string]string{}
|
||||
newConfigData["hosts"] = map[string]map[string]string{}
|
||||
|
||||
topLevelKeys := root.Content[0].Content
|
||||
|
||||
for i, x := range topLevelKeys {
|
||||
if x.Value == "" {
|
||||
continue
|
||||
}
|
||||
if i+1 == len(topLevelKeys) {
|
||||
break
|
||||
}
|
||||
hostname := x.Value
|
||||
newConfigData["hosts"][hostname] = map[string]string{}
|
||||
|
||||
authKeys := topLevelKeys[i+1].Content[0].Content
|
||||
|
||||
for j, y := range authKeys {
|
||||
if j+1 == len(authKeys) {
|
||||
break
|
||||
}
|
||||
switch y.Value {
|
||||
case "user":
|
||||
newConfigData["hosts"][hostname]["user"] = authKeys[j+1].Value
|
||||
case "oauth_token":
|
||||
newConfigData["hosts"][hostname]["oauth_token"] = authKeys[j+1].Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := newConfigData["hosts"][defaultHostname]; !ok {
|
||||
return errors.New("could not find default host configuration")
|
||||
}
|
||||
|
||||
defaultHostConfig := newConfigData["hosts"][defaultHostname]
|
||||
|
||||
if _, ok := defaultHostConfig["user"]; !ok {
|
||||
return errors.New("default host configuration missing user")
|
||||
}
|
||||
|
||||
if _, ok := defaultHostConfig["oauth_token"]; !ok {
|
||||
return errors.New("default host configuration missing oauth_token")
|
||||
}
|
||||
|
||||
newConfig, err := yaml.Marshal(newConfigData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = BackupConfigFile(fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to back up existing config: %s", err)
|
||||
}
|
||||
|
||||
return WriteConfigFile(fn, newConfig)
|
||||
}
|
||||
|
||||
func ParseConfig(fn string) (Config, error) {
|
||||
_, root, err := parseConfigFile(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isLegacy(root) {
|
||||
err = migrateConfig(fn, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, root, err = parseConfigFile(fn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reparse migrated config: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return NewConfig(root), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,88 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseConfig(t *testing.T) {
|
||||
c := strings.NewReader(`---
|
||||
github.com:
|
||||
- user: monalisa
|
||||
oauth_token: OTOKEN
|
||||
protocol: https
|
||||
- user: wronguser
|
||||
oauth_token: NOTTHIS
|
||||
`)
|
||||
entry, err := parseConfig(c)
|
||||
defer StubConfig(`---
|
||||
hosts:
|
||||
github.com:
|
||||
user: monalisa
|
||||
oauth_token: OTOKEN
|
||||
`)()
|
||||
config, err := ParseConfig("filename")
|
||||
eq(t, err, nil)
|
||||
eq(t, entry.User, "monalisa")
|
||||
eq(t, entry.Token, "OTOKEN")
|
||||
user, err := config.Get("github.com", "user")
|
||||
eq(t, err, nil)
|
||||
eq(t, user, "monalisa")
|
||||
token, err := config.Get("github.com", "oauth_token")
|
||||
eq(t, err, nil)
|
||||
eq(t, token, "OTOKEN")
|
||||
}
|
||||
|
||||
func Test_parseConfig_multipleHosts(t *testing.T) {
|
||||
c := strings.NewReader(`---
|
||||
example.com:
|
||||
- user: wronguser
|
||||
oauth_token: NOTTHIS
|
||||
github.com:
|
||||
- user: monalisa
|
||||
oauth_token: OTOKEN
|
||||
`)
|
||||
entry, err := parseConfig(c)
|
||||
defer StubConfig(`---
|
||||
hosts:
|
||||
example.com:
|
||||
user: wronguser
|
||||
oauth_token: NOTTHIS
|
||||
github.com:
|
||||
user: monalisa
|
||||
oauth_token: OTOKEN
|
||||
`)()
|
||||
config, err := ParseConfig("filename")
|
||||
eq(t, err, nil)
|
||||
eq(t, entry.User, "monalisa")
|
||||
eq(t, entry.Token, "OTOKEN")
|
||||
user, err := config.Get("github.com", "user")
|
||||
eq(t, err, nil)
|
||||
eq(t, user, "monalisa")
|
||||
token, err := config.Get("github.com", "oauth_token")
|
||||
eq(t, err, nil)
|
||||
eq(t, token, "OTOKEN")
|
||||
}
|
||||
|
||||
func Test_parseConfig_notFound(t *testing.T) {
|
||||
c := strings.NewReader(`---
|
||||
example.com:
|
||||
- user: wronguser
|
||||
oauth_token: NOTTHIS
|
||||
`)
|
||||
_, err := parseConfig(c)
|
||||
defer StubConfig(`---
|
||||
hosts:
|
||||
example.com:
|
||||
user: wronguser
|
||||
oauth_token: NOTTHIS
|
||||
`)()
|
||||
config, err := ParseConfig("filename")
|
||||
eq(t, err, nil)
|
||||
_, err = config.configForHost("github.com")
|
||||
eq(t, err, errors.New(`could not find config entry for "github.com"`))
|
||||
}
|
||||
|
||||
func Test_migrateConfig(t *testing.T) {
|
||||
oldStyle := `---
|
||||
github.com:
|
||||
- user: keiyuri
|
||||
oauth_token: 123456`
|
||||
|
||||
var root yaml.Node
|
||||
err := yaml.Unmarshal([]byte(oldStyle), &root)
|
||||
if err != nil {
|
||||
panic("failed to parse test yaml")
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
defer StubWriteConfig(buf)()
|
||||
|
||||
defer StubBackupConfig()()
|
||||
|
||||
err = migrateConfig("boom.txt", &root)
|
||||
eq(t, err, nil)
|
||||
|
||||
expected := `hosts:
|
||||
github.com:
|
||||
oauth_token: "123456"
|
||||
user: keiyuri
|
||||
`
|
||||
|
||||
eq(t, buf.String(), expected)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ var (
|
|||
|
||||
// TODO: have a conversation about whether this belongs in the "context" package
|
||||
// FIXME: make testable
|
||||
func setupConfigFile(filename string) (*configEntry, error) {
|
||||
func setupConfigFile(filename string) (Config, error) {
|
||||
var verboseStream io.Writer
|
||||
if strings.Contains(os.Getenv("DEBUG"), "oauth") {
|
||||
verboseStream = os.Stderr
|
||||
|
|
@ -54,29 +54,37 @@ func setupConfigFile(filename string) (*configEntry, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := configEntry{
|
||||
User: userLogin,
|
||||
Token: token,
|
||||
|
||||
// TODO this sucks. It precludes us laying out a nice config with comments and such.
|
||||
type yamlConfig struct {
|
||||
Hosts map[string]map[string]string
|
||||
}
|
||||
|
||||
yamlHosts := map[string]map[string]string{}
|
||||
yamlHosts[flow.Hostname] = map[string]string{}
|
||||
yamlHosts[flow.Hostname]["user"] = userLogin
|
||||
yamlHosts[flow.Hostname]["oauth_token"] = token
|
||||
|
||||
defaultConfig := yamlConfig{
|
||||
Hosts: yamlHosts,
|
||||
}
|
||||
data := make(map[string][]configEntry)
|
||||
data[flow.Hostname] = []configEntry{entry}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(filename), 0771)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
cfgFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer config.Close()
|
||||
defer cfgFile.Close()
|
||||
|
||||
yamlData, err := yaml.Marshal(data)
|
||||
yamlData, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := config.Write(yamlData)
|
||||
n, err := cfgFile.Write(yamlData)
|
||||
if err == nil && n < len(yamlData) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
|
|
@ -86,7 +94,8 @@ func setupConfigFile(filename string) (*configEntry, error) {
|
|||
_ = waitForEnter(os.Stdin)
|
||||
}
|
||||
|
||||
return &entry, err
|
||||
// TODO cleaner error handling? this "should" always work given that we /just/ wrote the file...
|
||||
return ParseConfig(filename)
|
||||
}
|
||||
|
||||
func getViewer(token string) (string, error) {
|
||||
|
|
|
|||
205
context/config_type.go
Normal file
205
context/config_type.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const defaultHostname = "github.com"
|
||||
const defaultGitProtocol = "https"
|
||||
|
||||
// This interface describes interacting with some persistent configuration for gh.
|
||||
type Config interface {
|
||||
Hosts() ([]*HostConfig, error)
|
||||
Get(string, string) (string, error)
|
||||
Set(string, string, string) error
|
||||
Write() error
|
||||
configForHost(string) (*HostConfig, error)
|
||||
parseHosts(*yaml.Node) ([]*HostConfig, error)
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
error
|
||||
}
|
||||
|
||||
type HostConfig struct {
|
||||
ConfigMap
|
||||
Host string
|
||||
}
|
||||
|
||||
// This type implements a low-level get/set config that is backed by an in-memory tree of Yaml
|
||||
// nodes. It allows us to interact with a yaml-based config programmatically, preserving any
|
||||
// comments that were present when the yaml waas parsed.
|
||||
type ConfigMap struct {
|
||||
Root *yaml.Node
|
||||
}
|
||||
|
||||
func (cm *ConfigMap) GetStringValue(key string) (string, error) {
|
||||
_, valueNode, err := cm.FindEntry(key)
|
||||
var notFound *NotFoundError
|
||||
|
||||
if err != nil && errors.As(err, ¬Found) {
|
||||
return defaultFor(key), nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if valueNode.Value == "" {
|
||||
return defaultFor(key), nil
|
||||
}
|
||||
|
||||
return valueNode.Value, nil
|
||||
}
|
||||
|
||||
func (cm *ConfigMap) SetStringValue(key, value string) error {
|
||||
_, valueNode, err := cm.FindEntry(key)
|
||||
|
||||
var notFound *NotFoundError
|
||||
|
||||
if err != nil && errors.As(err, ¬Found) {
|
||||
keyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: key,
|
||||
}
|
||||
valueNode = &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: "",
|
||||
}
|
||||
|
||||
cm.Root.Content = append(cm.Root.Content, keyNode, valueNode)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valueNode.Value = value
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *ConfigMap) FindEntry(key string) (keyNode, valueNode *yaml.Node, err error) {
|
||||
err = nil
|
||||
|
||||
topLevelKeys := cm.Root.Content
|
||||
for i, v := range topLevelKeys {
|
||||
if v.Value == key && i+1 < len(topLevelKeys) {
|
||||
keyNode = v
|
||||
valueNode = topLevelKeys[i+1]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, &NotFoundError{errors.New("not found")}
|
||||
}
|
||||
|
||||
func NewConfig(root *yaml.Node) Config {
|
||||
return &fileConfig{
|
||||
ConfigMap: ConfigMap{Root: root.Content[0]},
|
||||
documentRoot: root,
|
||||
}
|
||||
}
|
||||
|
||||
// This type implements a Config interface and represents a config file on disk.
|
||||
type fileConfig struct {
|
||||
ConfigMap
|
||||
documentRoot *yaml.Node
|
||||
hosts []*HostConfig
|
||||
}
|
||||
|
||||
func (c *fileConfig) Get(hostname, key string) (string, error) {
|
||||
if hostname == "" {
|
||||
return c.GetStringValue(key)
|
||||
} else {
|
||||
hostCfg, err := c.configForHost(hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hostCfg.GetStringValue(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fileConfig) Set(hostname, key, value string) error {
|
||||
if hostname == "" {
|
||||
return c.SetStringValue(key, value)
|
||||
} else {
|
||||
hostCfg, err := c.configForHost(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return hostCfg.SetStringValue(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) {
|
||||
hosts, err := c.Hosts()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hosts config: %s", err)
|
||||
}
|
||||
|
||||
for _, hc := range hosts {
|
||||
if hc.Host == hostname {
|
||||
return hc, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("could not find config entry for %q", hostname)
|
||||
}
|
||||
|
||||
func (c *fileConfig) Write() error {
|
||||
marshalled, err := yaml.Marshal(c.documentRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return WriteConfigFile(configFile(), marshalled)
|
||||
}
|
||||
|
||||
func (c *fileConfig) Hosts() ([]*HostConfig, error) {
|
||||
if len(c.hosts) > 0 {
|
||||
return c.hosts, nil
|
||||
}
|
||||
|
||||
_, hostsEntry, err := c.FindEntry("hosts")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find hosts config: %s", err)
|
||||
}
|
||||
|
||||
hostConfigs, err := c.parseHosts(hostsEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse hosts config: %s", err)
|
||||
}
|
||||
|
||||
c.hosts = hostConfigs
|
||||
|
||||
return hostConfigs, nil
|
||||
}
|
||||
|
||||
func (c *fileConfig) parseHosts(hostsEntry *yaml.Node) ([]*HostConfig, error) {
|
||||
hostConfigs := []*HostConfig{}
|
||||
|
||||
for i := 0; i < len(hostsEntry.Content)-1; i = i + 2 {
|
||||
hostname := hostsEntry.Content[i].Value
|
||||
hostRoot := hostsEntry.Content[i+1]
|
||||
hostConfig := HostConfig{
|
||||
ConfigMap: ConfigMap{Root: hostRoot},
|
||||
Host: hostname,
|
||||
}
|
||||
hostConfigs = append(hostConfigs, &hostConfig)
|
||||
}
|
||||
|
||||
if len(hostConfigs) == 0 {
|
||||
return nil, errors.New("could not find any host configurations")
|
||||
}
|
||||
|
||||
return hostConfigs, nil
|
||||
}
|
||||
|
||||
func defaultFor(key string) string {
|
||||
// we only have a set default for one setting right now
|
||||
switch key {
|
||||
case "git_protocol":
|
||||
return defaultGitProtocol
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ type Context interface {
|
|||
Remotes() (Remotes, error)
|
||||
BaseRepo() (ghrepo.Interface, error)
|
||||
SetBaseRepo(string)
|
||||
Config() (Config, error)
|
||||
}
|
||||
|
||||
// cap the number of git remotes looked up, since the user might have an
|
||||
|
|
@ -151,7 +152,7 @@ func New() Context {
|
|||
|
||||
// A Context implementation that queries the filesystem
|
||||
type fsContext struct {
|
||||
config *configEntry
|
||||
config Config
|
||||
remotes Remotes
|
||||
branch string
|
||||
baseRepo ghrepo.Interface
|
||||
|
|
@ -167,13 +168,13 @@ func configFile() string {
|
|||
return path.Join(ConfigDir(), "config.yml")
|
||||
}
|
||||
|
||||
func (c *fsContext) getConfig() (*configEntry, error) {
|
||||
func (c *fsContext) Config() (Config, error) {
|
||||
if c.config == nil {
|
||||
entry, err := parseOrSetupConfigFile(configFile())
|
||||
config, err := parseOrSetupConfigFile(configFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.config = entry
|
||||
c.config = config
|
||||
c.authToken = ""
|
||||
}
|
||||
return c.config, nil
|
||||
|
|
@ -184,11 +185,12 @@ func (c *fsContext) AuthToken() (string, error) {
|
|||
return c.authToken, nil
|
||||
}
|
||||
|
||||
config, err := c.getConfig()
|
||||
config, err := c.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return config.Token, nil
|
||||
|
||||
return config.Get(defaultHostname, "oauth_token")
|
||||
}
|
||||
|
||||
func (c *fsContext) SetAuthToken(t string) {
|
||||
|
|
@ -196,11 +198,12 @@ func (c *fsContext) SetAuthToken(t string) {
|
|||
}
|
||||
|
||||
func (c *fsContext) AuthLogin() (string, error) {
|
||||
config, err := c.getConfig()
|
||||
config, err := c.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return config.User, nil
|
||||
|
||||
return config.Get(defaultHostname, "user")
|
||||
}
|
||||
|
||||
func (c *fsContext) Branch() (string, error) {
|
||||
|
|
|
|||
46
context/testing.go
Normal file
46
context/testing.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func StubBackupConfig() func() {
|
||||
orig := BackupConfigFile
|
||||
BackupConfigFile = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func() {
|
||||
BackupConfigFile = orig
|
||||
}
|
||||
}
|
||||
|
||||
func StubWriteConfig(w io.Writer) func() {
|
||||
orig := WriteConfigFile
|
||||
WriteConfigFile = func(fn string, data []byte) error {
|
||||
_, err := w.Write(data)
|
||||
return err
|
||||
}
|
||||
return func() {
|
||||
WriteConfigFile = orig
|
||||
}
|
||||
}
|
||||
|
||||
func StubConfig(content string) func() {
|
||||
orig := ReadConfigFile
|
||||
ReadConfigFile = func(fn string) ([]byte, error) {
|
||||
return []byte(content), nil
|
||||
}
|
||||
return func() {
|
||||
ReadConfigFile = orig
|
||||
}
|
||||
}
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue