Merge remote-tracking branch 'origin/master' into heathy-helping

This commit is contained in:
Corey Johnson 2020-04-22 13:56:53 -07:00
commit 4dff85d3c1
28 changed files with 1137 additions and 271 deletions

View file

@ -10,7 +10,7 @@ import (
"strings"
"github.com/cli/cli/command"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/update"
"github.com/cli/cli/utils"
"github.com/mgutz/ansi"
@ -88,6 +88,6 @@ func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) {
}
repo := updaterEnabled
stateFilePath := path.Join(context.ConfigDir(), "state.yml")
stateFilePath := path.Join(config.ConfigDir(), "state.yml")
return update.CheckForUpdate(client, stateFilePath, repo, currentVersion)
}

112
command/config.go Normal file
View file

@ -0,0 +1,112 @@
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")
// TODO reveal and add usage once we properly support multiple hosts
_ = configGetCmd.Flags().MarkHidden("host")
// TODO reveal and add usage once we properly support multiple hosts
_ = configSetCmd.Flags().MarkHidden("host")
}
var configCmd = &cobra.Command{
Use: "config",
Short: "Set and get gh settings",
Long: `Get and set key/value strings.
Current respected settings:
- git_protocol: https or ssh. Default is https.
- editor: if unset, defaults to environment variables.
`,
}
var configGetCmd = &cobra.Command{
Use: "get <key>",
Short: "Prints the value of a given configuration key.",
Long: `Get the value for a given configuration key.
Examples:
$ gh config get git_protocol
https
`,
Args: cobra.ExactArgs(1),
RunE: configGet,
}
var configSetCmd = &cobra.Command{
Use: "set <key> <value>",
Short: "Updates configuration with the value of a given key.",
Long: `Update the configuration by setting a key to a value.
Examples:
$ gh config set editor vim
`,
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: %w", key, value, err)
}
err = cfg.Write()
if err != nil {
return fmt.Errorf("failed to write config to disk: %w", err)
}
return nil
}

191
command/config_test.go Normal file
View file

@ -0,0 +1,191 @@
package command
import (
"bytes"
"testing"
"github.com/cli/cli/internal/config"
)
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 config.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 config.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 TestConfigGetHost_unset(t *testing.T) {
cfg := `---
hosts:
github.com:
user: OWNER
oauth_token: MUSTBEHIGHCUZIMATOKEN
editor: ed
git_protocol: ssh
`
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 -hgithub.com git_protocol`: %v", err)
}
eq(t, output.String(), "ssh\n")
}
func TestConfigSetHost(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
buf := bytes.NewBufferString("")
defer config.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 config.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)
}

View file

@ -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")

View file

@ -47,7 +47,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
baseRemote, _ := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
// baseRemoteSpec is a repository URL or a remote name to be used in git fetch
baseURLOrName := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(baseRepo))
baseURLOrName := formatRemoteURL(cmd, ghrepo.FullName(baseRepo))
if baseRemote != nil {
baseURLOrName = baseRemote.Name
}
@ -97,7 +97,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
remote := baseURLOrName
mergeRef := ref
if pr.MaintainerCanModify {
remote = fmt.Sprintf("https://github.com/%s/%s.git", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
remote = formatRemoteURL(cmd, fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name))
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {

View file

@ -250,8 +250,8 @@ func prCreate(cmd *cobra.Command, _ []string) error {
// In either case, we want to add the head repo as a new git remote so we
// can push to it.
if headRemote == nil {
// TODO: support non-HTTPS git remote URLs
headRepoURL := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(headRepo))
headRepoURL := formatRemoteURL(cmd, ghrepo.FullName(headRepo))
// TODO: prevent clashes with another remote of a same name
gitRemote, err := git.AddRemote("fork", headRepoURL)
if err != nil {

View file

@ -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(`

View file

@ -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")

View file

@ -127,7 +127,7 @@ func runClone(cloneURL string, args []string) (target string, err error) {
func repoClone(cmd *cobra.Command, args []string) error {
cloneURL := args[0]
if !strings.Contains(cloneURL, ":") {
cloneURL = fmt.Sprintf("https://github.com/%s.git", cloneURL)
cloneURL = formatRemoteURL(cmd, cloneURL)
}
var repo ghrepo.Interface
@ -158,7 +158,7 @@ func repoClone(cmd *cobra.Command, args []string) error {
}
if parentRepo != nil {
err := addUpstreamRemote(parentRepo, cloneDir)
err := addUpstreamRemote(cmd, parentRepo, cloneDir)
if err != nil {
return err
}
@ -167,9 +167,8 @@ func repoClone(cmd *cobra.Command, args []string) error {
return nil
}
func addUpstreamRemote(parentRepo ghrepo.Interface, cloneDir string) error {
// TODO: support SSH remote URLs
upstreamURL := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(parentRepo))
func addUpstreamRemote(cmd *cobra.Command, parentRepo ghrepo.Interface, cloneDir string) error {
upstreamURL := formatRemoteURL(cmd, ghrepo.FullName(parentRepo))
cloneCmd := git.GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
cloneCmd.Stdout = os.Stdout
@ -267,14 +266,10 @@ func repoCreate(cmd *cobra.Command, args []string) error {
fmt.Fprintln(out, repo.URL)
}
remoteURL := repo.URL + ".git"
remoteURL := formatRemoteURL(cmd, ghrepo.FullName(repo))
if projectDirErr == nil {
// TODO: use git.AddRemote
remoteAdd := git.GitCommand("remote", "add", "origin", remoteURL)
remoteAdd.Stdout = os.Stdout
remoteAdd.Stderr = os.Stderr
err = run.PrepareCmd(remoteAdd).Run()
_, err = git.AddRemote("origin", remoteURL)
if err != nil {
return err
}
@ -338,7 +333,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to create client: %w", err)
}
var toFork ghrepo.Interface
var repoToFork ghrepo.Interface
inParent := false // whether or not we're forking the repo we're currently "in"
if len(args) == 0 {
baseRepo, err := determineBaseRepo(cmd, ctx)
@ -346,7 +341,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to determine base repository: %w", err)
}
inParent = true
toFork = baseRepo
repoToFork = baseRepo
} else {
repoArg := args[0]
@ -356,14 +351,23 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("did not understand argument: %w", err)
}
toFork, err = ghrepo.FromURL(parsedURL)
repoToFork, err = ghrepo.FromURL(parsedURL)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
} else if strings.HasPrefix(repoArg, "git@") {
parsedURL, err := git.ParseURL(repoArg)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
repoToFork, err = ghrepo.FromURL(parsedURL)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
} else {
toFork = ghrepo.FromFullName(repoArg)
if toFork.RepoName() == "" || toFork.RepoOwner() == "" {
repoToFork = ghrepo.FromFullName(repoArg)
if repoToFork.RepoName() == "" || repoToFork.RepoOwner() == "" {
return fmt.Errorf("could not parse owner or repo name from %s", repoArg)
}
}
@ -372,12 +376,12 @@ func repoFork(cmd *cobra.Command, args []string) error {
greenCheck := utils.Green("✓")
out := colorableOut(cmd)
s := utils.Spinner(out)
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(toFork))) + utils.Gray("...")
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(repoToFork))) + utils.Gray("...")
s.Suffix = " " + loading
s.FinalMSG = utils.Gray(fmt.Sprintf("- %s\n", loading))
s.Start()
forkedRepo, err := api.ForkRepo(apiClient, toFork)
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
if err != nil {
s.Stop()
return fmt.Errorf("failed to fork: %w", err)
@ -437,7 +441,9 @@ func repoFork(cmd *cobra.Command, args []string) error {
fmt.Fprintf(out, "%s Renamed %s remote to %s\n", greenCheck, utils.Bold(remoteName), utils.Bold(renameTarget))
}
_, err = git.AddRemote(remoteName, forkedRepo.CloneURL)
forkedRepoCloneURL := formatRemoteURL(cmd, ghrepo.FullName(forkedRepo))
_, err = git.AddRemote(remoteName, forkedRepoCloneURL)
if err != nil {
return fmt.Errorf("failed to add remote: %w", err)
}
@ -458,7 +464,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to clone fork: %w", err)
}
err = addUpstreamRemote(toFork, cloneDir)
err = addUpstreamRemote(cmd, repoToFork, cloneDir)
if err != nil {
return err
}

View file

@ -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")
@ -153,7 +153,7 @@ func TestRepoFork_in_parent_yes(t *testing.T) {
expectedCmds := []string{
"git remote rename origin upstream",
"git remote add -f origin https://github.com/someone/repo.git",
"git remote add -f origin https://github.com/someone/REPO.git",
}
for x, cmd := range seenCmds {
@ -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")
@ -287,7 +287,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
expectedCmds := []string{
"git remote rename origin upstream",
"git remote add -f origin https://github.com/someone/repo.git",
"git remote add -f origin https://github.com/someone/REPO.git",
}
for x, cmd := range seenCmds {
@ -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")
@ -499,7 +499,11 @@ func TestRepoCreate(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/OWNER/REPO"
"url": "https://github.com/OWNER/REPO",
"name": "REPO",
"owner": {
"login": "OWNER"
}
}
} } }
`))
@ -522,7 +526,7 @@ func TestRepoCreate(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/OWNER/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/OWNER/REPO.git")
var reqBody struct {
Query string
@ -564,7 +568,11 @@ func TestRepoCreate_org(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO"
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }
`))
@ -587,7 +595,7 @@ func TestRepoCreate_org(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/ORG/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string
@ -629,7 +637,11 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO"
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }
`))
@ -652,7 +664,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/ORG/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string
@ -678,7 +690,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 +785,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 +813,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 +840,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("{}"))

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/utils"
@ -17,6 +18,9 @@ import (
"github.com/spf13/pflag"
)
// TODO these are sprinkled across command, context, config, and ghrepo
const defaultHostname = "github.com"
// Version is dynamically set by the toolchain or overridden by the Makefile.
var Version = "DEV"
@ -104,9 +108,21 @@ func BasicClient() (*api.Client, error) {
opts = append(opts, apiVerboseLog())
}
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)))
c, err := config.ParseDefaultConfig()
if err != nil {
return nil, err
}
token, err := c.Get(defaultHostname, "oauth_token")
if err != nil {
return nil, err
}
if token == "" {
return nil, fmt.Errorf("no oauth_token set in config")
}
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
return api.NewClient(opts...), nil
}
@ -271,3 +287,22 @@ func includes(a []string, s string) bool {
}
return false
}
func formatRemoteURL(cmd *cobra.Command, fullRepoName string) string {
ctx := contextForCommand(cmd)
protocol := "https"
cfg, err := ctx.Config()
if err != nil {
fmt.Fprintf(colorableErr(cmd), "%s failed to load config: %s. using defaults\n", utils.Yellow("!"), err)
} else {
cfgProtocol, _ := cfg.Get(defaultHostname, "git_protocol")
protocol = cfgProtocol
}
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s.git", defaultHostname, fullRepoName)
}
return fmt.Sprintf("https://%s/%s.git", defaultHostname, fullRepoName)
}

View file

@ -40,3 +40,22 @@ func TestChangelogURL(t *testing.T) {
t.Errorf("expected %s to create url %s but got %s", tag, url, result)
}
}
func TestRemoteURLFormatting_no_config(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
eq(t, result, "https://github.com/OWNER/REPO.git")
}
func TestRemoteURLFormatting_ssh_config(t *testing.T) {
cfg := `---
hosts:
github.com:
user: OWNER
oauth_token: MUSTBEHIGHCUZIMATOKEN
git_protocol: ssh
`
initBlankContext(cfg, "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
eq(t, result, "git@github.com:OWNER/REPO.git")
}

View file

@ -10,8 +10,15 @@ import (
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
)
const defaultTestConfig = `hosts:
github.com:
user: OWNER
oauth_token: 1234567890
`
type askStubber struct {
Asks [][]*survey.Question
Count int
@ -63,7 +70,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 +78,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.
config.StubConfig(cfg)
return ctx
}
}

View file

@ -2,6 +2,7 @@ package command
import (
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/pkg/githubtemplate"
@ -82,6 +83,16 @@ func selectTemplate(templatePaths []string) (string, error) {
}
func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, defs defaults, templatePaths []string) (*titleBody, error) {
editorCommand := os.Getenv("GH_EDITOR")
if editorCommand == "" {
ctx := contextForCommand(cmd)
cfg, err := ctx.Config()
if err != nil {
return nil, fmt.Errorf("could not read config: %w", err)
}
editorCommand, _ = cfg.Get(defaultHostname, "editor")
}
var inProgress titleBody
inProgress.Title = defs.Title
templateContents := ""
@ -109,6 +120,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
bodyQuestion := &survey.Question{
Name: "body",
Prompt: &surveyext.GhEditor{
EditorCommand: editorCommand,
Editor: &survey.Editor{
Message: "Body",
FileName: "*.md",

View file

@ -5,6 +5,7 @@ import (
"strings"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
)
@ -22,6 +23,14 @@ type blankContext struct {
remotes Remotes
}
func (c *blankContext) Config() (config.Config, error) {
cfg, err := config.ParseConfig("boom.txt")
if err != nil {
panic(fmt.Sprintf("failed to parse config during tests. did you remember to stub? error: %s", err))
}
return cfg, nil
}
func (c *blankContext) AuthToken() (string, error) {
return c.authToken, nil
}

View file

@ -1,66 +0,0 @@
package context
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"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)
if err != nil && errors.Is(err, os.ErrNotExist) {
return setupConfigFile(fn)
}
return entry, err
}
func parseConfigFile(fn string) (*configEntry, 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)
if err != nil {
return nil, err
}
var config yaml.Node
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}
if len(config.Content) < 1 {
return nil, fmt.Errorf("malformed config")
}
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 nil, fmt.Errorf("could not find config entry for %q", defaultHostname)
}

View file

@ -1,55 +0,0 @@
package context
import (
"errors"
"reflect"
"strings"
"testing"
)
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)
eq(t, err, nil)
eq(t, entry.User, "monalisa")
eq(t, entry.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)
eq(t, err, nil)
eq(t, entry.User, "monalisa")
eq(t, entry.Token, "OTOKEN")
}
func Test_parseConfig_notFound(t *testing.T) {
c := strings.NewReader(`---
example.com:
- user: wronguser
oauth_token: NOTTHIS
`)
_, err := parseConfig(c)
eq(t, err, errors.New(`could not find config entry for "github.com"`))
}

View file

@ -3,15 +3,17 @@ package context
import (
"errors"
"fmt"
"path"
"sort"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
"github.com/mitchellh/go-homedir"
)
// TODO these are sprinkled across command, context, config, and ghrepo
const defaultHostname = "github.com"
// Context represents the interface for querying information about the current environment
type Context interface {
AuthToken() (string, error)
@ -22,6 +24,7 @@ type Context interface {
Remotes() (Remotes, error)
BaseRepo() (ghrepo.Interface, error)
SetBaseRepo(string)
Config() (config.Config, error)
}
// cap the number of git remotes looked up, since the user might have an
@ -151,29 +154,20 @@ func New() Context {
// A Context implementation that queries the filesystem
type fsContext struct {
config *configEntry
config config.Config
remotes Remotes
branch string
baseRepo ghrepo.Interface
authToken string
}
func ConfigDir() string {
dir, _ := homedir.Expand("~/.config/gh")
return dir
}
func configFile() string {
return path.Join(ConfigDir(), "config.yml")
}
func (c *fsContext) getConfig() (*configEntry, error) {
func (c *fsContext) Config() (config.Config, error) {
if c.config == nil {
entry, err := parseOrSetupConfigFile(configFile())
config, err := config.ParseOrSetupConfigFile(config.ConfigFile())
if err != nil {
return nil, err
}
c.config = entry
c.config = config
c.authToken = ""
}
return c.config, nil
@ -184,11 +178,17 @@ func (c *fsContext) AuthToken() (string, error) {
return c.authToken, nil
}
config, err := c.getConfig()
cfg, err := c.Config()
if err != nil {
return "", err
}
return config.Token, nil
token, err := cfg.Get(defaultHostname, "oauth_token")
if token == "" || err != nil {
return "", err
}
return token, nil
}
func (c *fsContext) SetAuthToken(t string) {
@ -196,11 +196,17 @@ 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
login, err := config.Get(defaultHostname, "user")
if login == "" || err != nil {
return "", err
}
return login, nil
}
func (c *fsContext) Branch() (string, error) {

View file

@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"net/url"
"reflect"
"testing"
"github.com/cli/cli/api"
@ -11,6 +12,13 @@ import (
"github.com/cli/cli/internal/ghrepo"
)
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_Remotes_FindByName(t *testing.T) {
list := Remotes{
&Remote{Remote: &git.Remote{Name: "mona"}, Owner: "monalisa", Repo: "myfork"},

View file

@ -79,9 +79,19 @@ func AddRemote(name, u string) (*Remote, error) {
return nil, err
}
urlParsed, err := url.Parse(u)
if err != nil {
return nil, err
var urlParsed *url.URL
if strings.HasPrefix(u, "https") {
urlParsed, err = url.Parse(u)
if err != nil {
return nil, err
}
} else {
urlParsed, err = ParseURL(u)
if err != nil {
return nil, err
}
}
return &Remote{

View file

@ -0,0 +1,181 @@
package config
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v3"
)
func ConfigDir() string {
dir, _ := homedir.Expand("~/.config/gh")
return dir
}
func ConfigFile() string {
return path.Join(ConfigDir(), "config.yml")
}
func ParseOrSetupConfigFile(fn string) (Config, error) {
config, err := ParseConfig(fn)
if err != nil && errors.Is(err, os.ErrNotExist) {
return setupConfigFile(fn)
}
return config, err
}
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()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
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 err
}
defer cfgFile.Close()
n, err := cfgFile.Write(data)
if err == nil && n < len(data) {
err = io.ErrShortWrite
}
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 true
}
func migrateConfig(fn string, root *yaml.Node) error {
type ConfigEntry map[string]string
type ConfigHash map[string]ConfigEntry
newConfigData := map[string]ConfigHash{}
newConfigData["hosts"] = ConfigHash{}
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] = ConfigEntry{}
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: %w", 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: %w", err)
}
}
return NewConfig(root), nil
}

View file

@ -0,0 +1,96 @@
package config
import (
"bytes"
"errors"
"reflect"
"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) {
defer StubConfig(`---
hosts:
github.com:
user: monalisa
oauth_token: OTOKEN
`)()
config, err := ParseConfig("filename")
eq(t, err, nil)
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) {
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)
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) {
defer StubConfig(`---
hosts:
example.com:
user: wronguser
oauth_token: NOTTHIS
`)()
config, err := ParseConfig("filename")
eq(t, err, nil)
_, err = config.Get("github.com", "user")
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)
}

View file

@ -1,4 +1,4 @@
package context
package config
import (
"bufio"
@ -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) {

View file

@ -1,4 +1,4 @@
package context
package config
const oauthSuccessPage = `
<!doctype html>

View file

@ -0,0 +1,218 @@
package config
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
}
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)
if err != nil {
return "", err
}
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, &notFound) {
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 != "" {
hostCfg, err := c.configForHost(hostname)
if err != nil {
return "", err
}
hostValue, err := hostCfg.GetStringValue(key)
var notFound *NotFoundError
if err != nil && !errors.As(err, &notFound) {
return "", err
}
if hostValue != "" {
return hostValue, nil
}
}
value, err := c.GetStringValue(key)
var notFound *NotFoundError
if err != nil && errors.As(err, &notFound) {
return defaultFor(key), nil
} else if err != nil {
return "", err
}
if value == "" {
return defaultFor(key), nil
}
return value, nil
}
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: %w", 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: %w", err)
}
hostConfigs, err := c.parseHosts(hostsEntry)
if err != nil {
return nil, fmt.Errorf("could not parse hosts config: %w", 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 ""
}
}

View file

@ -0,0 +1,37 @@
package config
import (
"io"
)
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
}
}

View file

@ -6,6 +6,7 @@ import (
"strings"
)
// TODO these are sprinkled across command, context, config, and ghrepo
const defaultHostname = "github.com"
// Interface describes an object that represents a GitHub repository

View file

@ -18,25 +18,34 @@ import (
)
var (
bom = []byte{0xef, 0xbb, 0xbf}
editor = "nano" // EXTENDED to switch from vim as a default editor
bom = []byte{0xef, 0xbb, 0xbf}
defaultEditor = "nano" // EXTENDED to switch from vim as a default editor
)
func init() {
if runtime.GOOS == "windows" {
editor = "notepad"
defaultEditor = "notepad"
} else if g := os.Getenv("GIT_EDITOR"); g != "" {
editor = g
defaultEditor = g
} else if v := os.Getenv("VISUAL"); v != "" {
editor = v
defaultEditor = v
} else if e := os.Getenv("EDITOR"); e != "" {
editor = e
defaultEditor = e
}
}
// EXTENDED to enable different prompting behavior
type GhEditor struct {
*survey.Editor
EditorCommand string
}
func (e *GhEditor) editorCommand() string {
if e.EditorCommand == "" {
return defaultEditor
}
return e.EditorCommand
}
// EXTENDED to change prompt text
@ -49,17 +58,17 @@ var EditorQuestionTemplate = `
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- color "cyan"}}[(e) to launch {{ .EditorName }}, enter to skip] {{color "reset"}}
{{- color "cyan"}}[(e) to launch {{ .EditorCommand }}, enter to skip] {{color "reset"}}
{{- end}}`
// EXTENDED to pass editor name (to use in prompt)
type EditorTemplateData struct {
survey.Editor
EditorName string
Answer string
ShowAnswer bool
ShowHelp bool
Config *survey.PromptConfig
EditorCommand string
Answer string
ShowAnswer bool
ShowHelp bool
Config *survey.PromptConfig
}
// EXTENDED to augment prompt text and keypress handling
@ -68,9 +77,9 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
EditorQuestionTemplate,
// EXTENDED to support printing editor in prompt
EditorTemplateData{
Editor: *e.Editor,
EditorName: filepath.Base(editor),
Config: config,
Editor: *e.Editor,
EditorCommand: filepath.Base(e.editorCommand()),
Config: config,
},
)
if err != nil {
@ -109,10 +118,10 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
EditorQuestionTemplate,
EditorTemplateData{
// EXTENDED to support printing editor in prompt
Editor: *e.Editor,
EditorName: filepath.Base(editor),
ShowHelp: true,
Config: config,
Editor: *e.Editor,
EditorCommand: filepath.Base(e.editorCommand()),
ShowHelp: true,
Config: config,
},
)
if err != nil {
@ -155,7 +164,7 @@ func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (int
stdio := e.Stdio()
args, err := shellquote.Split(editor)
args, err := shellquote.Split(e.editorCommand())
if err != nil {
return "", err
}