Merge branch 'trunk' of https://github.com/cli/cli into feature/repo-with-gitignore-license
This commit is contained in:
commit
7c8b6867f4
54 changed files with 2543 additions and 923 deletions
77
pkg/cmd/extensions/command.go
Normal file
77
pkg/cmd/extensions/command.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdExtensions(io *iostreams.IOStreams) *cobra.Command {
|
||||
m := NewManager()
|
||||
|
||||
extCmd := cobra.Command{
|
||||
Use: "extensions",
|
||||
Short: "Manage gh extensions",
|
||||
}
|
||||
|
||||
extCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List installed extension commands",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmds := m.List()
|
||||
if len(cmds) == 0 {
|
||||
return errors.New("no extensions installed")
|
||||
}
|
||||
for _, c := range cmds {
|
||||
name := filepath.Base(c)
|
||||
parts := strings.SplitN(name, "-", 2)
|
||||
fmt.Fprintf(io.Out, "%s %s\n", parts[0], parts[1])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "install <repo>",
|
||||
Short: "Install a gh extension from a repository",
|
||||
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "." {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.InstallLocal(wd)
|
||||
}
|
||||
repo, err := ghrepo.FromFullName(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(repo.RepoName(), "gh-") {
|
||||
return errors.New("the repository name must start with `gh-`")
|
||||
}
|
||||
protocol := "https" // TODO: respect user's preferred protocol
|
||||
return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrade installed extensions",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return m.Upgrade(io.Out, io.ErrOut)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
extCmd.Hidden = true
|
||||
return &extCmd
|
||||
}
|
||||
121
pkg/cmd/extensions/manager.go
Normal file
121
pkg/cmd/extensions/manager.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
dataDir func() string
|
||||
lookPath func(string) (string, error)
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
dataDir: config.ConfigDir,
|
||||
lookPath: safeexec.LookPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) {
|
||||
if len(args) == 0 {
|
||||
return false, errors.New("too few arguments in list")
|
||||
}
|
||||
|
||||
var exe string
|
||||
extName := "gh-" + args[0]
|
||||
forwardArgs := args[1:]
|
||||
|
||||
for _, e := range m.List() {
|
||||
if filepath.Base(e) == extName {
|
||||
exe = e
|
||||
break
|
||||
}
|
||||
}
|
||||
if exe == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO: parse the shebang on Windows and invoke the correct interpreter instead of invoking directly
|
||||
externalCmd := exec.Command(exe, forwardArgs...)
|
||||
externalCmd.Stdin = stdin
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
return true, externalCmd.Run()
|
||||
}
|
||||
|
||||
func (m *Manager) List() []string {
|
||||
dir := m.installDir()
|
||||
entries, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, f := range entries {
|
||||
if !strings.HasPrefix(f.Name(), "gh-") || !(f.IsDir() || f.Mode()&os.ModeSymlink != 0) {
|
||||
continue
|
||||
}
|
||||
results = append(results, filepath.Join(dir, f.Name(), f.Name()))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (m *Manager) InstallLocal(dir string) error {
|
||||
name := filepath.Base(dir)
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
return os.Symlink(dir, targetDir)
|
||||
}
|
||||
|
||||
func (m *Manager) Install(cloneURL string, stdout, stderr io.Writer) error {
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(path.Base(cloneURL), ".git")
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
|
||||
externalCmd := exec.Command(exe, "clone", cloneURL, targetDir)
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
return externalCmd.Run()
|
||||
}
|
||||
|
||||
func (m *Manager) Upgrade(stdout, stderr io.Writer) error {
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exts := m.List()
|
||||
if len(exts) == 0 {
|
||||
return errors.New("no extensions installed")
|
||||
}
|
||||
|
||||
for _, f := range exts {
|
||||
fmt.Fprintf(stdout, "[%s]: ", filepath.Base(f))
|
||||
dir := filepath.Dir(f)
|
||||
externalCmd := exec.Command(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
if e := externalCmd.Run(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) installDir() string {
|
||||
return filepath.Join(m.dataDir(), "extensions")
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
|
|
@ -14,11 +16,93 @@ import (
|
|||
)
|
||||
|
||||
func New(appVersion string) *cmdutil.Factory {
|
||||
io := iostreams.System()
|
||||
f := &cmdutil.Factory{
|
||||
Config: configFunc(), // No factory dependencies
|
||||
Branch: branchFunc(), // No factory dependencies
|
||||
Executable: executable(), // No factory dependencies
|
||||
}
|
||||
|
||||
f.IOStreams = ioStreams(f) // Depends on Config
|
||||
f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion
|
||||
f.Remotes = remotesFunc(f) // Depends on Config
|
||||
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
|
||||
f.Browser = browser(f) // Depends on IOStreams
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
remotes, err := f.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return remotes[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
func SmartBaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
remotes, err := f.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseRepo, err := repoContext.BaseRepo(f.IOStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseRepo, nil
|
||||
}
|
||||
}
|
||||
|
||||
func remotesFunc(f *cmdutil.Factory) func() (context.Remotes, error) {
|
||||
rr := &remoteResolver{
|
||||
readRemotes: git.Remotes,
|
||||
getConfig: f.Config,
|
||||
}
|
||||
return rr.Resolver()
|
||||
}
|
||||
|
||||
func httpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client, error) {
|
||||
return func() (*http.Client, error) {
|
||||
io := f.IOStreams
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewHTTPClient(io, cfg, appVersion, true), nil
|
||||
}
|
||||
}
|
||||
|
||||
func browser(f *cmdutil.Factory) cmdutil.Browser {
|
||||
io := f.IOStreams
|
||||
return cmdutil.NewBrowser(os.Getenv("BROWSER"), io.Out, io.ErrOut)
|
||||
}
|
||||
|
||||
func executable() string {
|
||||
gh := "gh"
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
gh = exe
|
||||
}
|
||||
return gh
|
||||
}
|
||||
|
||||
func configFunc() func() (config.Config, error) {
|
||||
var cachedConfig config.Config
|
||||
var configError error
|
||||
configFunc := func() (config.Config, error) {
|
||||
return func() (config.Config, error) {
|
||||
if cachedConfig != nil || configError != nil {
|
||||
return cachedConfig, configError
|
||||
}
|
||||
|
|
@ -30,45 +114,38 @@ func New(appVersion string) *cmdutil.Factory {
|
|||
cachedConfig = config.InheritEnv(cachedConfig)
|
||||
return cachedConfig, configError
|
||||
}
|
||||
}
|
||||
|
||||
rr := &remoteResolver{
|
||||
readRemotes: git.Remotes,
|
||||
getConfig: configFunc,
|
||||
}
|
||||
remotesFunc := rr.Resolver()
|
||||
|
||||
ghExecutable := "gh"
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
ghExecutable = exe
|
||||
}
|
||||
|
||||
return &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
Config: configFunc,
|
||||
Remotes: remotesFunc,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
cfg, err := configFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewHTTPClient(io, cfg, appVersion, true), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
remotes, err := remotesFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return remotes[0], nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
currentBranch, err := git.CurrentBranch()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current branch: %w", err)
|
||||
}
|
||||
return currentBranch, nil
|
||||
},
|
||||
Executable: ghExecutable,
|
||||
Browser: cmdutil.NewBrowser(os.Getenv("BROWSER"), io.Out, io.ErrOut),
|
||||
func branchFunc() func() (string, error) {
|
||||
return func() (string, error) {
|
||||
currentBranch, err := git.CurrentBranch()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current branch: %w", err)
|
||||
}
|
||||
return currentBranch, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
|
||||
io := iostreams.System()
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return io
|
||||
}
|
||||
|
||||
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
|
||||
io.SetNeverPrompt(true)
|
||||
}
|
||||
|
||||
// Pager precedence
|
||||
// 1. GH_PAGER
|
||||
// 2. pager from config
|
||||
// 3. PAGER
|
||||
if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists {
|
||||
io.SetPager(ghPager)
|
||||
} else if pager, _ := cfg.Get("", "pager"); pager != "" {
|
||||
io.SetPager(pager)
|
||||
}
|
||||
|
||||
return io
|
||||
}
|
||||
|
|
|
|||
391
pkg/cmd/factory/default_test.go
Normal file
391
pkg/cmd/factory/default_test.go
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_BaseRepo(t *testing.T) {
|
||||
orig_GH_HOST := os.Getenv("GH_HOST")
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("GH_HOST", orig_GH_HOST)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remotes git.RemoteSet
|
||||
config config.Config
|
||||
override string
|
||||
wantsErr bool
|
||||
wantsName string
|
||||
wantsOwner string
|
||||
wantsHost string
|
||||
}{
|
||||
{
|
||||
name: "matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
wantsName: "repo",
|
||||
wantsOwner: "owner",
|
||||
wantsHost: "nonsense.com",
|
||||
},
|
||||
{
|
||||
name: "no matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://test.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "override with matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://test.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsName: "repo",
|
||||
wantsOwner: "owner",
|
||||
wantsHost: "test.com",
|
||||
},
|
||||
{
|
||||
name: "override with no matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.override != "" {
|
||||
os.Setenv("GH_HOST", tt.override)
|
||||
} else {
|
||||
os.Unsetenv("GH_HOST")
|
||||
}
|
||||
f := New("1")
|
||||
rr := &remoteResolver{
|
||||
readRemotes: func() (git.RemoteSet, error) {
|
||||
return tt.remotes, nil
|
||||
},
|
||||
getConfig: func() (config.Config, error) {
|
||||
return tt.config, nil
|
||||
},
|
||||
}
|
||||
f.Remotes = rr.Resolver()
|
||||
f.BaseRepo = BaseRepoFunc(f)
|
||||
repo, err := f.BaseRepo()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantsName, repo.RepoName())
|
||||
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
|
||||
assert.Equal(t, tt.wantsHost, repo.RepoHost())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SmartBaseRepo(t *testing.T) {
|
||||
pu, _ := url.Parse("https://test.com/newowner/newrepo.git")
|
||||
orig_GH_HOST := os.Getenv("GH_HOST")
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("GH_HOST", orig_GH_HOST)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remotes git.RemoteSet
|
||||
config config.Config
|
||||
override string
|
||||
wantsErr bool
|
||||
wantsName string
|
||||
wantsOwner string
|
||||
wantsHost string
|
||||
}{
|
||||
{
|
||||
name: "override with matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://test.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsName: "repo",
|
||||
wantsOwner: "owner",
|
||||
wantsHost: "test.com",
|
||||
},
|
||||
{
|
||||
name: "override with matching remote and base resolution",
|
||||
remotes: git.RemoteSet{
|
||||
&git.Remote{Name: "origin",
|
||||
Resolved: "base",
|
||||
FetchURL: pu,
|
||||
PushURL: pu},
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsName: "newrepo",
|
||||
wantsOwner: "newowner",
|
||||
wantsHost: "test.com",
|
||||
},
|
||||
{
|
||||
name: "override with matching remote and nonbase resolution",
|
||||
remotes: git.RemoteSet{
|
||||
&git.Remote{Name: "origin",
|
||||
Resolved: "johnny/test",
|
||||
FetchURL: pu,
|
||||
PushURL: pu},
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsName: "test",
|
||||
wantsOwner: "johnny",
|
||||
wantsHost: "test.com",
|
||||
},
|
||||
{
|
||||
name: "override with no matching remote",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://example.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
override: "test.com",
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.override != "" {
|
||||
os.Setenv("GH_HOST", tt.override)
|
||||
} else {
|
||||
os.Unsetenv("GH_HOST")
|
||||
}
|
||||
f := New("1")
|
||||
rr := &remoteResolver{
|
||||
readRemotes: func() (git.RemoteSet, error) {
|
||||
return tt.remotes, nil
|
||||
},
|
||||
getConfig: func() (config.Config, error) {
|
||||
return tt.config, nil
|
||||
},
|
||||
}
|
||||
f.Remotes = rr.Resolver()
|
||||
f.BaseRepo = SmartBaseRepoFunc(f)
|
||||
repo, err := f.BaseRepo()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantsName, repo.RepoName())
|
||||
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
|
||||
assert.Equal(t, tt.wantsHost, repo.RepoHost())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Defined in pkg/cmdutil/repo_override.go but test it along with other BaseRepo functions
|
||||
func Test_OverrideBaseRepo(t *testing.T) {
|
||||
orig_GH_HOST := os.Getenv("GH_REPO")
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("GH_REPO", orig_GH_HOST)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remotes git.RemoteSet
|
||||
config config.Config
|
||||
envOverride string
|
||||
argOverride string
|
||||
wantsErr bool
|
||||
wantsName string
|
||||
wantsOwner string
|
||||
wantsHost string
|
||||
}{
|
||||
{
|
||||
name: "override from argument",
|
||||
argOverride: "override/test",
|
||||
wantsHost: "github.com",
|
||||
wantsOwner: "override",
|
||||
wantsName: "test",
|
||||
},
|
||||
{
|
||||
name: "override from environment",
|
||||
envOverride: "somehost.com/override/test",
|
||||
wantsHost: "somehost.com",
|
||||
wantsOwner: "override",
|
||||
wantsName: "test",
|
||||
},
|
||||
{
|
||||
name: "no override",
|
||||
remotes: git.RemoteSet{
|
||||
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
|
||||
},
|
||||
config: defaultConfig(),
|
||||
wantsHost: "nonsense.com",
|
||||
wantsOwner: "owner",
|
||||
wantsName: "repo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.envOverride != "" {
|
||||
os.Setenv("GH_REPO", tt.envOverride)
|
||||
} else {
|
||||
os.Unsetenv("GH_REPO")
|
||||
}
|
||||
f := New("1")
|
||||
rr := &remoteResolver{
|
||||
readRemotes: func() (git.RemoteSet, error) {
|
||||
return tt.remotes, nil
|
||||
},
|
||||
getConfig: func() (config.Config, error) {
|
||||
return tt.config, nil
|
||||
},
|
||||
}
|
||||
f.Remotes = rr.Resolver()
|
||||
f.BaseRepo = cmdutil.OverrideBaseRepoFunc(f, tt.argOverride)
|
||||
repo, err := f.BaseRepo()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantsName, repo.RepoName())
|
||||
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
|
||||
assert.Equal(t, tt.wantsHost, repo.RepoHost())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ioStreams_pager(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
config config.Config
|
||||
wantPager string
|
||||
}{
|
||||
{
|
||||
name: "GH_PAGER and PAGER set",
|
||||
env: map[string]string{
|
||||
"GH_PAGER": "GH_PAGER",
|
||||
"PAGER": "PAGER",
|
||||
},
|
||||
wantPager: "GH_PAGER",
|
||||
},
|
||||
{
|
||||
name: "GH_PAGER and config pager set",
|
||||
env: map[string]string{
|
||||
"GH_PAGER": "GH_PAGER",
|
||||
},
|
||||
config: pagerConfig(),
|
||||
wantPager: "GH_PAGER",
|
||||
},
|
||||
{
|
||||
name: "config pager and PAGER set",
|
||||
env: map[string]string{
|
||||
"PAGER": "PAGER",
|
||||
},
|
||||
config: pagerConfig(),
|
||||
wantPager: "CONFIG_PAGER",
|
||||
},
|
||||
{
|
||||
name: "only PAGER set",
|
||||
env: map[string]string{
|
||||
"PAGER": "PAGER",
|
||||
},
|
||||
wantPager: "PAGER",
|
||||
},
|
||||
{
|
||||
name: "GH_PAGER set to blank string",
|
||||
env: map[string]string{
|
||||
"GH_PAGER": "",
|
||||
"PAGER": "PAGER",
|
||||
},
|
||||
wantPager: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.env != nil {
|
||||
for k, v := range tt.env {
|
||||
old := os.Getenv(k)
|
||||
os.Setenv(k, v)
|
||||
if k == "GH_PAGER" {
|
||||
defer os.Unsetenv(k)
|
||||
} else {
|
||||
defer os.Setenv(k, old)
|
||||
}
|
||||
}
|
||||
}
|
||||
f := New("1")
|
||||
if tt.config != nil {
|
||||
f.Config = func() (config.Config, error) {
|
||||
return tt.config, nil
|
||||
}
|
||||
}
|
||||
io := ioStreams(f)
|
||||
assert.Equal(t, tt.wantPager, io.GetPager())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ioStreams_prompt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config config.Config
|
||||
promptDisabled bool
|
||||
}{
|
||||
{
|
||||
name: "default config",
|
||||
promptDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "config with prompt disabled",
|
||||
config: disablePromptConfig(),
|
||||
promptDisabled: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := New("1")
|
||||
if tt.config != nil {
|
||||
f.Config = func() (config.Config, error) {
|
||||
return tt.config, nil
|
||||
}
|
||||
}
|
||||
io := ioStreams(f)
|
||||
assert.Equal(t, tt.promptDisabled, io.GetNeverPrompt())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig() config.Config {
|
||||
return config.InheritEnv(config.NewFromString(heredoc.Doc(`
|
||||
hosts:
|
||||
nonsense.com:
|
||||
oauth_token: BLAH
|
||||
`)))
|
||||
}
|
||||
|
||||
func pagerConfig() config.Config {
|
||||
return config.NewFromString("pager: CONFIG_PAGER")
|
||||
}
|
||||
|
||||
func disablePromptConfig() config.Config {
|
||||
return config.NewFromString("prompt: disabled")
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
)
|
||||
|
|
@ -53,8 +52,12 @@ var timezoneNames = map[int]string{
|
|||
50400: "Pacific/Kiritimati",
|
||||
}
|
||||
|
||||
type configGetter interface {
|
||||
Get(string, string) (string, error)
|
||||
}
|
||||
|
||||
// generic authenticated HTTP client for commands
|
||||
func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, setAccept bool) *http.Client {
|
||||
func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string, setAccept bool) *http.Client {
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
logTraffic := strings.Contains(verbose, "api")
|
||||
|
|
@ -64,7 +67,7 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
|
|||
opts = append(opts,
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)),
|
||||
api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) {
|
||||
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
|
||||
hostname := ghinstance.NormalizeHostname(getHost(req))
|
||||
if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" {
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}
|
||||
|
|
@ -85,13 +88,10 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
|
|||
if setAccept {
|
||||
opts = append(opts,
|
||||
api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) {
|
||||
// antiope-preview: Checks
|
||||
accept := "application/vnd.github.antiope-preview+json"
|
||||
// introduced for #2952: pr branch up to date status
|
||||
accept += ", application/vnd.github.merge-info-preview+json"
|
||||
if ghinstance.IsEnterprise(req.URL.Hostname()) {
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
accept += ", application/vnd.github.shadow-cat-preview"
|
||||
accept := "application/vnd.github.merge-info-preview+json" // PullRequest.mergeStateStatus
|
||||
if ghinstance.IsEnterprise(getHost(req)) {
|
||||
accept += ", application/vnd.github.antiope-preview" // Commit.statusCheckRollup
|
||||
accept += ", application/vnd.github.shadow-cat-preview" // PullRequest.isDraft
|
||||
}
|
||||
return accept, nil
|
||||
}),
|
||||
|
|
@ -100,3 +100,10 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
|
|||
|
||||
return api.NewHTTPClient(opts...)
|
||||
}
|
||||
|
||||
func getHost(r *http.Request) string {
|
||||
if r.Host != "" {
|
||||
return r.Host
|
||||
}
|
||||
return r.URL.Hostname()
|
||||
}
|
||||
|
|
|
|||
174
pkg/cmd/factory/http_test.go
Normal file
174
pkg/cmd/factory/http_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewHTTPClient(t *testing.T) {
|
||||
type args struct {
|
||||
config configGetter
|
||||
appVersion string
|
||||
setAccept bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
envDebug string
|
||||
host string
|
||||
wantHeader map[string]string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "github.com with Accept header",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token MYTOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "application/vnd.github.merge-info-preview+json",
|
||||
},
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "github.com no Accept header",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: false,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token MYTOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "",
|
||||
},
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "github.com no authentication token",
|
||||
args: args{
|
||||
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "application/vnd.github.merge-info-preview+json",
|
||||
},
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "github.com in verbose mode",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
},
|
||||
host: "github.com",
|
||||
envDebug: "api",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token MYTOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "application/vnd.github.merge-info-preview+json",
|
||||
},
|
||||
wantStderr: heredoc.Doc(`
|
||||
* Request at <time>
|
||||
* Request to http://<host>:<port>
|
||||
> GET / HTTP/1.1
|
||||
> Host: github.com
|
||||
> Accept: application/vnd.github.merge-info-preview+json
|
||||
> Authorization: token ████████████████████
|
||||
> User-Agent: GitHub CLI v1.2.3
|
||||
|
||||
< HTTP/1.1 204 No Content
|
||||
< Date: <time>
|
||||
|
||||
* Request took <duration>
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "GHES Accept header",
|
||||
args: args{
|
||||
config: tinyConfig{"example.com:oauth_token": "GHETOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
},
|
||||
host: "example.com",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token GHETOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.antiope-preview, application/vnd.github.shadow-cat-preview",
|
||||
},
|
||||
wantStderr: "",
|
||||
},
|
||||
}
|
||||
|
||||
var gotReq *http.Request
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotReq = r
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oldDebug := os.Getenv("DEBUG")
|
||||
os.Setenv("DEBUG", tt.envDebug)
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("DEBUG", oldDebug)
|
||||
})
|
||||
|
||||
io, _, _, stderr := iostreams.Test()
|
||||
client := NewHTTPClient(io, tt.args.config, tt.args.appVersion, tt.args.setAccept)
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
req.Host = tt.host
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
for name, value := range tt.wantHeader {
|
||||
assert.Equal(t, value, gotReq.Header.Get(name), name)
|
||||
}
|
||||
|
||||
assert.Equal(t, 204, res.StatusCode)
|
||||
assert.Equal(t, tt.wantStderr, normalizeVerboseLog(stderr.String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type tinyConfig map[string]string
|
||||
|
||||
func (c tinyConfig) Get(host, key string) (string, error) {
|
||||
return c[fmt.Sprintf("%s:%s", host, key)], nil
|
||||
}
|
||||
|
||||
var requestAtRE = regexp.MustCompile(`(?m)^\* Request at .+`)
|
||||
var dateRE = regexp.MustCompile(`(?m)^< Date: .+`)
|
||||
var hostWithPortRE = regexp.MustCompile(`127\.0\.0\.1:\d+`)
|
||||
var durationRE = regexp.MustCompile(`(?m)^\* Request took .+`)
|
||||
|
||||
func normalizeVerboseLog(t string) string {
|
||||
t = requestAtRE.ReplaceAllString(t, "* Request at <time>")
|
||||
t = hostWithPortRE.ReplaceAllString(t, "<host>:<port>")
|
||||
t = dateRE.ReplaceAllString(t, "< Date: <time>")
|
||||
t = durationRE.ReplaceAllString(t, "* Request took <duration>")
|
||||
return t
|
||||
}
|
||||
|
|
@ -234,10 +234,6 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tb.Body == "" {
|
||||
tb.Body = templateContent
|
||||
}
|
||||
}
|
||||
|
||||
openURL, err = generatePreviewURL(apiClient, baseRepo, tb)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
args, err := shlex.Split(tt.cli)
|
||||
require.NoError(t, err)
|
||||
cmd.SetArgs(args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -168,7 +170,7 @@ func Test_createRun(t *testing.T) {
|
|||
WebMode: true,
|
||||
Assignees: []string{"monalisa"},
|
||||
},
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=monalisa",
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=monalisa&body=",
|
||||
wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -185,7 +187,7 @@ func Test_createRun(t *testing.T) {
|
|||
"viewer": { "login": "MonaLisa" }
|
||||
} }`))
|
||||
},
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa",
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=",
|
||||
wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -214,7 +216,7 @@ func Test_createRun(t *testing.T) {
|
|||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }`))
|
||||
},
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?projects=OWNER%2FREPO%2F1",
|
||||
wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=&projects=OWNER%2FREPO%2F1",
|
||||
wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
|
|||
milestoneTitle = issue.Milestone.Title
|
||||
}
|
||||
fmt.Fprintf(out, "milestone:\t%s\n", milestoneTitle)
|
||||
fmt.Fprintf(out, "number:\t%d\n", issue.Number)
|
||||
fmt.Fprintln(out, "--")
|
||||
fmt.Fprintln(out, issue.Body)
|
||||
return nil
|
||||
|
|
@ -172,7 +173,7 @@ func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error {
|
|||
cs := opts.IO.ColorScheme()
|
||||
|
||||
// Header (Title and State)
|
||||
fmt.Fprintln(out, cs.Bold(issue.Title))
|
||||
fmt.Fprintf(out, "%s #%d\n", cs.Bold(issue.Title), issue.Number)
|
||||
fmt.Fprintf(out,
|
||||
"%s • %s opened %s • %s\n",
|
||||
issueStateTitleWithColor(cs, issue.State),
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ func TestIssueView_nontty_Preview(t *testing.T) {
|
|||
`comments:\t9`,
|
||||
`author:\tmarseilles`,
|
||||
`assignees:`,
|
||||
`number:\t123\n`,
|
||||
`\*\*bold story\*\*`,
|
||||
},
|
||||
},
|
||||
|
|
@ -126,6 +127,7 @@ func TestIssueView_nontty_Preview(t *testing.T) {
|
|||
`labels:\tone, two, three, four, five`,
|
||||
`projects:\tProject 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
|
||||
`milestone:\tuluru\n`,
|
||||
`number:\t123\n`,
|
||||
`\*\*bold story\*\*`,
|
||||
},
|
||||
},
|
||||
|
|
@ -136,6 +138,7 @@ func TestIssueView_nontty_Preview(t *testing.T) {
|
|||
`state:\tOPEN`,
|
||||
`author:\tmarseilles`,
|
||||
`labels:\ttarot`,
|
||||
`number:\t123\n`,
|
||||
},
|
||||
},
|
||||
"Closed issue": {
|
||||
|
|
@ -146,6 +149,7 @@ func TestIssueView_nontty_Preview(t *testing.T) {
|
|||
`\*\*bold story\*\*`,
|
||||
`author:\tmarseilles`,
|
||||
`labels:\ttarot`,
|
||||
`number:\t123\n`,
|
||||
`\*\*bold story\*\*`,
|
||||
},
|
||||
},
|
||||
|
|
@ -178,7 +182,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
|
|||
"Open issue without metadata": {
|
||||
fixture: "./fixtures/issueView_preview.json",
|
||||
expectedOutputs: []string{
|
||||
`ix of coins`,
|
||||
`ix of coins #123`,
|
||||
`Open.*marseilles opened about 9 years ago.*9 comments`,
|
||||
`bold story`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
|
|
@ -187,7 +191,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
|
|||
"Open issue with metadata": {
|
||||
fixture: "./fixtures/issueView_previewWithMetadata.json",
|
||||
expectedOutputs: []string{
|
||||
`ix of coins`,
|
||||
`ix of coins #123`,
|
||||
`Open.*marseilles opened about 9 years ago.*9 comments`,
|
||||
`8 \x{1f615} • 7 \x{1f440} • 6 \x{2764}\x{fe0f} • 5 \x{1f389} • 4 \x{1f604} • 3 \x{1f680} • 2 \x{1f44e} • 1 \x{1f44d}`,
|
||||
`Assignees:.*marseilles, monaco\n`,
|
||||
|
|
@ -201,7 +205,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
|
|||
"Open issue with empty body": {
|
||||
fixture: "./fixtures/issueView_previewWithEmptyBody.json",
|
||||
expectedOutputs: []string{
|
||||
`ix of coins`,
|
||||
`ix of coins #123`,
|
||||
`Open.*marseilles opened about 9 years ago.*9 comments`,
|
||||
`No description provided`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
|
|
@ -210,7 +214,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
|
|||
"Closed issue": {
|
||||
fixture: "./fixtures/issueView_previewClosedState.json",
|
||||
expectedOutputs: []string{
|
||||
`ix of coins`,
|
||||
`ix of coins #123`,
|
||||
`Closed.*marseilles opened about 9 years ago.*9 comments`,
|
||||
`bold story`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
|
|
@ -310,7 +314,7 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some title #123`,
|
||||
`some body`,
|
||||
`———————— Not showing 5 comments ————————`,
|
||||
`marseilles \(Collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
|
|
@ -326,7 +330,7 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
"CommentsForIssue": "./fixtures/issueView_previewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some title #123`,
|
||||
`some body`,
|
||||
`monalisa • Jan 1, 2020 • Edited`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`,
|
||||
|
|
@ -386,6 +390,7 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
`state:\tOPEN`,
|
||||
`author:\tmarseilles`,
|
||||
`comments:\t6`,
|
||||
`number:\t123`,
|
||||
`some body`,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -302,10 +302,6 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if state.Body == "" {
|
||||
state.Body = templateContent
|
||||
}
|
||||
}
|
||||
|
||||
openURL, err = generateCompareURL(*ctx, *state)
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ func TestPRCreate_nontty_web(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1", output.BrowsedURL)
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
|
||||
}
|
||||
|
||||
func TestPRCreate_recover(t *testing.T) {
|
||||
|
|
@ -780,7 +780,7 @@ func TestPRCreate_web(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1", output.BrowsedURL)
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL)
|
||||
}
|
||||
|
||||
func TestPRCreate_webLongURL(t *testing.T) {
|
||||
|
|
@ -851,7 +851,7 @@ func TestPRCreate_webProject(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1&projects=ORG%2F1", output.BrowsedURL)
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", output.BrowsedURL)
|
||||
}
|
||||
|
||||
func Test_determineTrackingBranch_empty(t *testing.T) {
|
||||
|
|
@ -965,7 +965,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
BaseBranch: "main",
|
||||
HeadBranchLabel: "feature",
|
||||
},
|
||||
want: "https://github.com/OWNER/REPO/compare/main...feature?expand=1",
|
||||
want: "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -978,7 +978,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
state: prShared.IssueMetadataState{
|
||||
Labels: []string{"one", "two three"},
|
||||
},
|
||||
want: "https://github.com/OWNER/REPO/compare/a...b?expand=1&labels=one%2Ctwo+three",
|
||||
want: "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -988,7 +988,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
BaseBranch: "main/trunk",
|
||||
HeadBranchLabel: "owner:feature",
|
||||
},
|
||||
want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?expand=1",
|
||||
want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?body=&expand=1",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package shared
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/shlex"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/githubsearch"
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState) (string, error) {
|
||||
|
|
@ -20,9 +20,10 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba
|
|||
if state.Title != "" {
|
||||
q.Set("title", state.Title)
|
||||
}
|
||||
if state.Body != "" {
|
||||
q.Set("body", state.Body)
|
||||
}
|
||||
// We always want to send the body parameter, even if it's empty, to prevent the web interface from
|
||||
// applying the default template. Since the user has the option to select a template in the terminal,
|
||||
// assume that empty body here means that the user either skipped it or erased its contents.
|
||||
q.Set("body", state.Body)
|
||||
if len(state.Assignees) > 0 {
|
||||
q.Set("assignees", strings.Join(state.Assignees, ","))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
|
@ -9,6 +8,7 @@ import (
|
|||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_listURLWithQuery(t *testing.T) {
|
||||
|
|
@ -192,3 +192,56 @@ func Test_QueryHasStateClause(t *testing.T) {
|
|||
assert.Equal(t, tt.hasState, gotState)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_WithPrAndIssueQueryParams(t *testing.T) {
|
||||
type args struct {
|
||||
baseURL string
|
||||
state IssueMetadataState
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "blank",
|
||||
args: args{
|
||||
baseURL: "",
|
||||
state: IssueMetadataState{},
|
||||
},
|
||||
want: "?body=",
|
||||
},
|
||||
{
|
||||
name: "no values",
|
||||
args: args{
|
||||
baseURL: "http://example.com/hey",
|
||||
state: IssueMetadataState{},
|
||||
},
|
||||
want: "http://example.com/hey?body=",
|
||||
},
|
||||
{
|
||||
name: "title and body",
|
||||
args: args{
|
||||
baseURL: "http://example.com/hey",
|
||||
state: IssueMetadataState{
|
||||
Title: "my title",
|
||||
Body: "my bodeh",
|
||||
},
|
||||
},
|
||||
want: "http://example.com/hey?body=my+bodeh&title=my+title",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := WithPrAndIssueQueryParams(nil, nil, tt.args.baseURL, tt.args.state)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("WithPrAndIssueQueryParams() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("WithPrAndIssueQueryParams() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string
|
|||
return err
|
||||
}
|
||||
|
||||
if state.Body != "" && preBody != state.Body {
|
||||
if preBody != state.Body {
|
||||
state.MarkDirty()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"requestedReviewer": {
|
||||
"__typename": "Team",
|
||||
"name": "Team 1"
|
||||
}
|
||||
"requestedReviewer": {
|
||||
"__typename": "Team",
|
||||
"name": "Team 1",
|
||||
"slug": "team-1",
|
||||
"organization": {"login": "my-org"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"requestedReviewer": {
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error {
|
|||
cs := opts.IO.ColorScheme()
|
||||
|
||||
// Header (Title and State)
|
||||
fmt.Fprintln(out, cs.Bold(pr.Title))
|
||||
fmt.Fprintf(out, "%s #%d\n", cs.Bold(pr.Title), pr.Number)
|
||||
fmt.Fprintf(out,
|
||||
"%s • %s wants to merge %s into %s from %s • %s %s \n",
|
||||
shared.StateTitleWithColor(cs, *pr),
|
||||
|
|
@ -294,8 +294,6 @@ func prReviewerList(pr api.PullRequest, cs *iostreams.ColorScheme) string {
|
|||
return reviewerList
|
||||
}
|
||||
|
||||
const teamTypeName = "Team"
|
||||
|
||||
const ghostName = "ghost"
|
||||
|
||||
// parseReviewers parses given Reviews and ReviewRequests
|
||||
|
|
@ -317,10 +315,7 @@ func parseReviewers(pr api.PullRequest) []*reviewerState {
|
|||
|
||||
// Overwrite reviewer's state if a review request for the same reviewer exists.
|
||||
for _, reviewRequest := range pr.ReviewRequests.Nodes {
|
||||
name := reviewRequest.RequestedReviewer.Login
|
||||
if reviewRequest.RequestedReviewer.TypeName == teamTypeName {
|
||||
name = reviewRequest.RequestedReviewer.Name
|
||||
}
|
||||
name := reviewRequest.RequestedReviewer.LoginOrSlug()
|
||||
reviewerStates[name] = &reviewerState{
|
||||
Name: name,
|
||||
State: requestedReviewState,
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
`milestone:\t\n`,
|
||||
`additions:\t100\n`,
|
||||
`deletions:\t10\n`,
|
||||
`reviewers:\tDEF \(Commented\), def \(Changes requested\), ghost \(Approved\), hubot \(Commented\), xyz \(Approved\), 123 \(Requested\), Team 1 \(Requested\), abc \(Requested\)\n`,
|
||||
`reviewers:\tDEF \(Commented\), def \(Changes requested\), ghost \(Approved\), hubot \(Commented\), xyz \(Approved\), 123 \(Requested\), abc \(Requested\), my-org\/team-1 \(Requested\)\n`,
|
||||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
|
|
@ -350,7 +350,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Open.*nobody wants to merge 12 commits into master from blueberries.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
|
|
@ -363,7 +363,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Open.*nobody wants to merge 12 commits into master from blueberries.+100.-10`,
|
||||
`Reviewers:.*1 \(.*Requested.*\)\n`,
|
||||
`Assignees:.*marseilles, monaco\n`,
|
||||
|
|
@ -382,8 +382,8 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"ReviewsForPullRequest": "./fixtures/prViewPreviewManyReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Reviewers:.*DEF \(.*Commented.*\), def \(.*Changes requested.*\), ghost \(.*Approved.*\), hubot \(Commented\), xyz \(.*Approved.*\), 123 \(.*Requested.*\), Team 1 \(.*Requested.*\), abc \(.*Requested.*\)\n`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Reviewers: DEF \(Commented\), def \(Changes requested\), ghost \(Approved\), hubot \(Commented\), xyz \(Approved\), 123 \(Requested\), abc \(Requested\), my-org\/team-1 \(Requested\)`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
},
|
||||
|
|
@ -395,7 +395,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Closed.*nobody wants to merge 12 commits into master from blueberries.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
|
|
@ -408,7 +408,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Merged.*nobody wants to merge 12 commits into master from blueberries.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
|
|
@ -421,7 +421,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Blueberries are from a fork #12`,
|
||||
`Draft.*nobody wants to merge 12 commits into master from blueberries.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
|
|
@ -501,7 +501,7 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
"ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some title #12`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f}`,
|
||||
`some body`,
|
||||
`———————— Not showing 9 comments ————————`,
|
||||
|
|
@ -521,7 +521,7 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
"CommentsForPullRequest": "./fixtures/prViewPreviewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some title #12`,
|
||||
`some body`,
|
||||
`monalisa • Jan 1, 2020 • Edited`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`,
|
||||
|
|
|
|||
|
|
@ -22,12 +22,15 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const defaultRemoteName = "origin"
|
||||
|
||||
type ForkOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Since func(time.Time) time.Duration
|
||||
|
||||
GitArgs []string
|
||||
Repository string
|
||||
|
|
@ -40,9 +43,9 @@ type ForkOptions struct {
|
|||
Rename bool
|
||||
}
|
||||
|
||||
var Since = func(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
// TODO warn about useless flags (--remote, --remote-name) when running from outside a repository
|
||||
// TODO output over STDOUT not STDERR
|
||||
// TODO remote-name has no effect on its own; error that or change behavior
|
||||
|
||||
func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Command {
|
||||
opts := &ForkOptions{
|
||||
|
|
@ -51,6 +54,7 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman
|
|||
Config: f.Config,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Remotes: f.Remotes,
|
||||
Since: time.Since,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -110,7 +114,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
|||
|
||||
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}")
|
||||
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}")
|
||||
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", "origin", "Specify a name for a fork's new remote.")
|
||||
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", defaultRemoteName, "Specify a name for a fork's new remote.")
|
||||
cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization")
|
||||
|
||||
return cmd
|
||||
|
|
@ -182,7 +186,7 @@ func forkRun(opts *ForkOptions) error {
|
|||
// returns the fork repo data even if it already exists -- with no change in status code or
|
||||
// anything. We thus check the created time to see if the repo is brand new or not; if it's not,
|
||||
// we assume the fork already existed and report an error.
|
||||
createdAgo := Since(forkedRepo.CreatedAt)
|
||||
createdAgo := opts.Since(forkedRepo.CreatedAt)
|
||||
if createdAgo > time.Minute {
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s %s %s\n",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -64,6 +64,12 @@ var HelpTopics = map[string]map[string]string{
|
|||
GH_NO_UPDATE_NOTIFIER: set to any value to disable update notifications. By default, gh
|
||||
checks for new releases once every 24 hours and displays an upgrade notice on standard
|
||||
error if a newer version was found.
|
||||
|
||||
GH_CONFIG_DIR, XDG_CONFIG_HOME (in order of precedence): the directory where gh will store configuration files.
|
||||
|
||||
XDG_STATE_HOME: the directory where gh will store state files.
|
||||
|
||||
XDG_DATA_HOME: the directory where gh will store data files.
|
||||
`),
|
||||
},
|
||||
"reference": {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
actionsCmd "github.com/cli/cli/pkg/cmd/actions"
|
||||
aliasCmd "github.com/cli/cli/pkg/cmd/alias"
|
||||
apiCmd "github.com/cli/cli/pkg/cmd/api"
|
||||
authCmd "github.com/cli/cli/pkg/cmd/auth"
|
||||
completionCmd "github.com/cli/cli/pkg/cmd/completion"
|
||||
configCmd "github.com/cli/cli/pkg/cmd/config"
|
||||
extensionsCmd "github.com/cli/cli/pkg/cmd/extensions"
|
||||
"github.com/cli/cli/pkg/cmd/factory"
|
||||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
|
|
@ -80,6 +78,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
|
||||
cmd.AddCommand(gistCmd.NewCmdGist(f))
|
||||
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
|
||||
cmd.AddCommand(extensionsCmd.NewCmdExtensions(f.IOStreams))
|
||||
cmd.AddCommand(secretCmd.NewCmdSecret(f))
|
||||
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
|
||||
|
||||
|
|
@ -91,7 +90,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
|
||||
// below here at the commands that require the "intelligent" BaseRepo resolver
|
||||
repoResolvingCmdFactory := *f
|
||||
repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo(f)
|
||||
repoResolvingCmdFactory.BaseRepo = factory.SmartBaseRepoFunc(f)
|
||||
|
||||
cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory))
|
||||
|
|
@ -124,29 +123,3 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er
|
|||
return factory.NewHTTPClient(f.IOStreams, cfg, version, false), nil
|
||||
}
|
||||
}
|
||||
|
||||
func resolvedBaseRepo(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
remotes, err := f.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseRepo, err := repoContext.BaseRepo(f.IOStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseRepo, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
|
|||
}
|
||||
|
||||
func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp {
|
||||
re := fmt.Sprintf(`%s\/%d_.*\.txt`, job.Name, step.Number)
|
||||
re := fmt.Sprintf(`%s\/%d_.*\.txt`, regexp.QuoteMeta(job.Name), step.Number)
|
||||
return regexp.MustCompile(re)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -926,6 +926,17 @@ func Test_attachRunLog(t *testing.T) {
|
|||
},
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "escape metacharacters in job name",
|
||||
job: shared.Job{
|
||||
Name: "metacharacters .+*?()|[]{}^$ job",
|
||||
Steps: []shared.Step{{
|
||||
Name: "fob the barz",
|
||||
Number: 0,
|
||||
}},
|
||||
},
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
name: "mismatching job name",
|
||||
job: shared.Job{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/secret/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -23,6 +26,7 @@ type ListOptions struct {
|
|||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
OrgName string
|
||||
EnvName string
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
|
|
@ -35,12 +39,16 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List secrets",
|
||||
Long: "List secrets for a repository or organization",
|
||||
Long: "List secrets for a repository, environment, or organization",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -50,18 +58,19 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
c, err := opts.HttpClient()
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create http client: %w", err)
|
||||
}
|
||||
client := api.NewClientFromHTTP(c)
|
||||
|
||||
orgName := opts.OrgName
|
||||
envName := opts.EnvName
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" {
|
||||
|
|
@ -73,7 +82,11 @@ func listRun(opts *ListOptions) error {
|
|||
|
||||
var secrets []*Secret
|
||||
if orgName == "" {
|
||||
secrets, err = getRepoSecrets(client, baseRepo)
|
||||
if envName == "" {
|
||||
secrets, err = getRepoSecrets(client, baseRepo)
|
||||
} else {
|
||||
secrets, err = getEnvSecrets(client, baseRepo, envName)
|
||||
}
|
||||
} else {
|
||||
var cfg config.Config
|
||||
var host string
|
||||
|
|
@ -145,7 +158,7 @@ func fmtVisibility(s Secret) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func getOrgSecrets(client *api.Client, host, orgName string) ([]*Secret, error) {
|
||||
func getOrgSecrets(client httpClient, host, orgName string) ([]*Secret, error) {
|
||||
secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -160,7 +173,7 @@ func getOrgSecrets(client *api.Client, host, orgName string) ([]*Secret, error)
|
|||
continue
|
||||
}
|
||||
var result responseData
|
||||
if err := client.REST(host, "GET", secret.SelectedReposURL, nil, &result); err != nil {
|
||||
if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err)
|
||||
}
|
||||
secret.NumSelectedRepos = result.TotalCount
|
||||
|
|
@ -169,7 +182,12 @@ func getOrgSecrets(client *api.Client, host, orgName string) ([]*Secret, error)
|
|||
return secrets, nil
|
||||
}
|
||||
|
||||
func getRepoSecrets(client *api.Client, repo ghrepo.Interface) ([]*Secret, error) {
|
||||
func getEnvSecrets(client httpClient, repo ghrepo.Interface, envName string) ([]*Secret, error) {
|
||||
path := fmt.Sprintf("repos/%s/environments/%s/secrets", ghrepo.FullName(repo), envName)
|
||||
return getSecrets(client, repo.RepoHost(), path)
|
||||
}
|
||||
|
||||
func getRepoSecrets(client httpClient, repo ghrepo.Interface) ([]*Secret, error) {
|
||||
return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets",
|
||||
ghrepo.FullName(repo)))
|
||||
}
|
||||
|
|
@ -178,13 +196,63 @@ type secretsPayload struct {
|
|||
Secrets []*Secret
|
||||
}
|
||||
|
||||
func getSecrets(client *api.Client, host, path string) ([]*Secret, error) {
|
||||
result := secretsPayload{}
|
||||
type httpClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
err := client.REST(host, "GET", path, nil, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func getSecrets(client httpClient, host, path string) ([]*Secret, error) {
|
||||
var results []*Secret
|
||||
url := fmt.Sprintf("%s%s?per_page=100", ghinstance.RESTPrefix(host), path)
|
||||
|
||||
for {
|
||||
var payload secretsPayload
|
||||
nextURL, err := apiGet(client, url, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, payload.Secrets...)
|
||||
|
||||
if nextURL == "" {
|
||||
break
|
||||
}
|
||||
url = nextURL
|
||||
}
|
||||
|
||||
return result.Secrets, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func apiGet(client httpClient, url string, data interface{}) (string, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return "", api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return findNextPage(resp.Header.Get("Link")), nil
|
||||
}
|
||||
|
||||
var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`)
|
||||
|
||||
func findNextPage(link string) string {
|
||||
for _, m := range linkRE.FindAllStringSubmatch(link, -1) {
|
||||
if len(m) >= 2 && m[2] == "next" {
|
||||
return m[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package list
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -38,6 +40,13 @@ func Test_NewCmdList(t *testing.T) {
|
|||
OrgName: "UmbrellaCorporation",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env",
|
||||
cli: "-eDevelopment",
|
||||
wants: ListOptions{
|
||||
EnvName: "Development",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -64,7 +73,7 @@ func Test_NewCmdList(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
|
||||
|
||||
assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -120,16 +129,44 @@ func Test_listRun(t *testing.T) {
|
|||
"SECRET_THREE\t1975-11-30\tSELECTED",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env tty",
|
||||
tty: true,
|
||||
opts: &ListOptions{
|
||||
EnvName: "Development",
|
||||
},
|
||||
wantOut: []string{
|
||||
"SECRET_ONE.*Updated 1988-10-11",
|
||||
"SECRET_TWO.*Updated 2020-12-04",
|
||||
"SECRET_THREE.*Updated 1975-11-30",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env not tty",
|
||||
tty: false,
|
||||
opts: &ListOptions{
|
||||
EnvName: "Development",
|
||||
},
|
||||
wantOut: []string{
|
||||
"SECRET_ONE\t1988-10-11",
|
||||
"SECRET_TWO\t2020-12-04",
|
||||
"SECRET_THREE\t1975-11-30",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
path := "repos/owner/repo/actions/secrets"
|
||||
if tt.opts.EnvName != "" {
|
||||
path = fmt.Sprintf("repos/owner/repo/environments/%s/secrets", tt.opts.EnvName)
|
||||
}
|
||||
|
||||
t0, _ := time.Parse("2006-01-02", "1988-10-11")
|
||||
t1, _ := time.Parse("2006-01-02", "2020-12-04")
|
||||
t2, _ := time.Parse("2006-01-02", "1975-11-30")
|
||||
path := "repos/owner/repo/actions/secrets"
|
||||
payload := secretsPayload{}
|
||||
payload.Secrets = []*Secret{
|
||||
{
|
||||
|
|
@ -200,3 +237,32 @@ func Test_listRun(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSecrets_pagination(t *testing.T) {
|
||||
var requests []*http.Request
|
||||
var client testClient = func(req *http.Request) (*http.Response, error) {
|
||||
header := make(map[string][]string)
|
||||
if len(requests) == 0 {
|
||||
header["Link"] = []string{`<http://example.com/page/0>; rel="previous", <http://example.com/page/2>; rel="next"`}
|
||||
}
|
||||
requests = append(requests, req)
|
||||
return &http.Response{
|
||||
Request: req,
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"secrets":[{},{}]}`)),
|
||||
Header: header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
secrets, err := getSecrets(client, "github.com", "path/to")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(requests))
|
||||
assert.Equal(t, 4, len(secrets))
|
||||
assert.Equal(t, "https://api.github.com/path/to?per_page=100", requests[0].URL.String())
|
||||
assert.Equal(t, "http://example.com/page/2", requests[1].URL.String())
|
||||
}
|
||||
|
||||
type testClient func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (c testClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return c(req)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type RemoveOptions struct {
|
|||
|
||||
SecretName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
}
|
||||
|
||||
func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Command {
|
||||
|
|
@ -31,12 +32,17 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
|
|||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove <secret-name>",
|
||||
Short: "Remove an organization or repository secret",
|
||||
Short: "Remove secrets",
|
||||
Long: "Remove a secret for a repository, environment, or organization",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.SecretName = args[0]
|
||||
|
||||
if runF != nil {
|
||||
|
|
@ -46,7 +52,8 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
|
|||
return removeRun(opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization")
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Remove a secret for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Remove a secret for an environment")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -59,6 +66,7 @@ func removeRun(opts *RemoveOptions) error {
|
|||
client := api.NewClientFromHTTP(c)
|
||||
|
||||
orgName := opts.OrgName
|
||||
envName := opts.EnvName
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" {
|
||||
|
|
@ -69,10 +77,12 @@ func removeRun(opts *RemoveOptions) error {
|
|||
}
|
||||
|
||||
var path string
|
||||
if orgName == "" {
|
||||
path = fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(baseRepo), opts.SecretName)
|
||||
} else {
|
||||
if orgName != "" {
|
||||
path = fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, opts.SecretName)
|
||||
} else if envName != "" {
|
||||
path = fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(baseRepo), envName, opts.SecretName)
|
||||
} else {
|
||||
path = fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(baseRepo), opts.SecretName)
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
|
|
@ -96,7 +106,11 @@ func removeRun(opts *RemoveOptions) error {
|
|||
target = ghrepo.FullName(baseRepo)
|
||||
}
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out, "%s Removed secret %s from %s\n", cs.SuccessIconWithColor(cs.Red), opts.SecretName, target)
|
||||
if envName != "" {
|
||||
fmt.Fprintf(opts.IO.Out, "%s Removed secret %s from %s environment on %s\n", cs.SuccessIconWithColor(cs.Red), opts.SecretName, envName, target)
|
||||
} else {
|
||||
fmt.Fprintf(opts.IO.Out, "%s Removed secret %s from %s\n", cs.SuccessIconWithColor(cs.Red), opts.SecretName, target)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ func TestNewCmdRemove(t *testing.T) {
|
|||
OrgName: "anOrg",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env",
|
||||
cli: "cool --env anEnv",
|
||||
wants: RemoveOptions{
|
||||
SecretName: "cool",
|
||||
EnvName: "anEnv",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -72,6 +80,7 @@ func TestNewCmdRemove(t *testing.T) {
|
|||
|
||||
assert.Equal(t, tt.wants.SecretName, gotOpts.SecretName)
|
||||
assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
|
||||
assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +115,36 @@ func Test_removeRun_repo(t *testing.T) {
|
|||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func Test_removeRun_env(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/owner/repo/environments/development/secrets/cool_secret"),
|
||||
httpmock.StatusStringResponse(204, "No Content"))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &RemoveOptions{
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
SecretName: "cool_secret",
|
||||
EnvName: "development",
|
||||
}
|
||||
|
||||
err := removeRun(opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func Test_removeRun_org(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ func NewCmdSecret(f *cmdutil.Factory) *cobra.Command {
|
|||
Use: "secret <command>",
|
||||
Short: "Manage GitHub secrets",
|
||||
Long: heredoc.Doc(`
|
||||
Secrets can be set at the repository or organization level for use in GitHub Actions.
|
||||
Run "gh help secret set" to learn how to get started.
|
||||
Secrets can be set at the repository, environment, or organization level for use in
|
||||
GitHub Actions. Run "gh help secret set" to learn how to get started.
|
||||
`),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ func getRepoPubKey(client *api.Client, repo ghrepo.Interface) (*PubKey, error) {
|
|||
ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
func getEnvPubKey(client *api.Client, repo ghrepo.Interface, envName string) (*PubKey, error) {
|
||||
return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/environments/%s/secrets/public-key",
|
||||
ghrepo.FullName(repo), envName))
|
||||
}
|
||||
|
||||
func putSecret(client *api.Client, host, path string, payload SecretPayload) error {
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
|
|
@ -90,6 +95,15 @@ func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions,
|
|||
return putSecret(client, host, path, payload)
|
||||
}
|
||||
|
||||
func putEnvSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, envName string, secretName, eValue string) error {
|
||||
payload := SecretPayload{
|
||||
EncryptedValue: eValue,
|
||||
KeyID: pk.ID,
|
||||
}
|
||||
path := fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(repo), envName, secretName)
|
||||
return putSecret(client, repo.RepoHost(), path, payload)
|
||||
}
|
||||
|
||||
func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string) error {
|
||||
payload := SecretPayload{
|
||||
EncryptedValue: eValue,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type SetOptions struct {
|
|||
|
||||
SecretName string
|
||||
OrgName string
|
||||
EnvName string
|
||||
Body string
|
||||
Visibility string
|
||||
RepositoryNames []string
|
||||
|
|
@ -48,7 +49,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
cmd := &cobra.Command{
|
||||
Use: "set <secret-name>",
|
||||
Short: "Create or update secrets",
|
||||
Long: "Locally encrypt a new or updated secret at either the repository or organization level and send it to GitHub for storage.",
|
||||
Long: "Locally encrypt a new or updated secret at either the repository, environment, or organization level and send it to GitHub for storage.",
|
||||
Example: heredoc.Doc(`
|
||||
Paste secret in prompt
|
||||
$ gh secret set MYSECRET
|
||||
|
|
@ -59,6 +60,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
Use file as secret value
|
||||
$ gh secret set MYSECRET < file.json
|
||||
|
||||
Set environment level secret
|
||||
$ gh secret set MYSECRET -bval --env=anEnv
|
||||
|
||||
Set organization level secret visible to entire organization
|
||||
$ gh secret set MYSECRET -bval --org=anOrg --visibility=all
|
||||
|
||||
|
|
@ -75,6 +79,10 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.SecretName = args[0]
|
||||
|
||||
err := validSecretName(opts.SecretName)
|
||||
|
|
@ -115,7 +123,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
return setRun(opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization")
|
||||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Set a secret for an organization")
|
||||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Set a secret for an environment")
|
||||
cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `all`, `private`, or `selected`")
|
||||
cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of repository names for `selected` visibility")
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "A value for the secret. Reads from STDIN if not specified.")
|
||||
|
|
@ -136,6 +145,7 @@ func setRun(opts *SetOptions) error {
|
|||
client := api.NewClientFromHTTP(c)
|
||||
|
||||
orgName := opts.OrgName
|
||||
envName := opts.EnvName
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" {
|
||||
|
|
@ -158,6 +168,8 @@ func setRun(opts *SetOptions) error {
|
|||
var pk *PubKey
|
||||
if orgName != "" {
|
||||
pk, err = getOrgPublicKey(client, host, orgName)
|
||||
} else if envName != "" {
|
||||
pk, err = getEnvPubKey(client, baseRepo, envName)
|
||||
} else {
|
||||
pk, err = getRepoPubKey(client, baseRepo)
|
||||
}
|
||||
|
|
@ -174,6 +186,8 @@ func setRun(opts *SetOptions) error {
|
|||
|
||||
if orgName != "" {
|
||||
err = putOrgSecret(client, host, pk, *opts, encoded)
|
||||
} else if envName != "" {
|
||||
err = putEnvSecret(client, pk, baseRepo, envName, opts.SecretName, encoded)
|
||||
} else {
|
||||
err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,17 @@ func TestNewCmdSet(t *testing.T) {
|
|||
OrgName: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env",
|
||||
cli: `cool_secret -b"a secret" -eRelease`,
|
||||
wants: SetOptions{
|
||||
SecretName: "cool_secret",
|
||||
Visibility: shared.Private,
|
||||
Body: "a secret",
|
||||
OrgName: "",
|
||||
EnvName: "Release",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vis all",
|
||||
cli: `cool_secret --org coolOrg -b"cool" -vall`,
|
||||
|
|
@ -160,6 +171,7 @@ func TestNewCmdSet(t *testing.T) {
|
|||
assert.Equal(t, tt.wants.Body, gotOpts.Body)
|
||||
assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
|
||||
assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
|
||||
assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
|
||||
assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
|
||||
})
|
||||
}
|
||||
|
|
@ -204,6 +216,46 @@ func Test_setRun_repo(t *testing.T) {
|
|||
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
||||
}
|
||||
|
||||
func Test_setRun_env(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
reg.Register(httpmock.REST("GET", "repos/owner/repo/environments/development/secrets/public-key"),
|
||||
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
||||
|
||||
reg.Register(httpmock.REST("PUT", "repos/owner/repo/environments/development/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &SetOptions{
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) { return config.NewBlankConfig(), nil },
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
EnvName: "development",
|
||||
IO: io,
|
||||
SecretName: "cool_secret",
|
||||
Body: "a secret",
|
||||
// Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7
|
||||
RandomOverride: bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}),
|
||||
}
|
||||
|
||||
err := setRun(opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reg.Verify(t)
|
||||
|
||||
data, err := ioutil.ReadAll(reg.Requests[1].Body)
|
||||
assert.NoError(t, err)
|
||||
var payload SecretPayload
|
||||
err = json.Unmarshal(data, &payload)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload.KeyID, "123")
|
||||
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
||||
}
|
||||
|
||||
func Test_setRun_org(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue