Merge remote-tracking branch 'origin' into 3704-credential-helper

This commit is contained in:
Mislav Marohnić 2021-09-24 14:35:01 +02:00
commit 78b35b7b6e
28 changed files with 840 additions and 203 deletions

View file

@ -33,7 +33,7 @@ jobs:
repository: github/cli.github.com
path: site
fetch-depth: 0
token: ${{secrets.SITE_GITHUB_TOKEN}}
ssh-key: ${{secrets.SITE_SSH_KEY}}
- name: Update site man pages
env:
GIT_COMMITTER_NAME: cli automation

View file

@ -16,8 +16,9 @@ type IssuesPayload struct {
}
type IssuesAndTotalCount struct {
Issues []Issue
TotalCount int
Issues []Issue
TotalCount int
SearchCapped bool
}
type Issue struct {

View file

@ -26,6 +26,7 @@ type PullRequestsPayload struct {
type PullRequestAndTotalCount struct {
TotalCount int
PullRequests []PullRequest
SearchCapped bool
}
type PullRequest struct {

View file

@ -24,7 +24,7 @@ commands is:
pkg/cmd/<command>/<subcommand>/<subcommand>.go
```
Following the above example, the main implementation for the `gh issue list` command, including its help
text, is in [pkg/cmd/issue/view/view.go](../pkg/cmd/issue/view/view.go)
text, is in [pkg/cmd/issue/list/list.go](../pkg/cmd/issue/list/list.go)
Other help topics not specific to any command, for example `gh help environment`, are found in
[pkg/cmd/root/help_topic.go](../pkg/cmd/root/help_topic.go).

View file

@ -14,6 +14,7 @@ func isSupportedProtocol(u string) bool {
strings.HasPrefix(u, "git+ssh:") ||
strings.HasPrefix(u, "git:") ||
strings.HasPrefix(u, "http:") ||
strings.HasPrefix(u, "git+https:") ||
strings.HasPrefix(u, "https:")
}
@ -43,6 +44,10 @@ func ParseURL(rawURL string) (u *url.URL, err error) {
u.Scheme = "ssh"
}
if u.Scheme == "git+https" {
u.Scheme = "https"
}
if u.Scheme != "ssh" {
return
}

View file

@ -28,11 +28,26 @@ func TestIsURL(t *testing.T) {
url: "git://example.com/owner/repo",
want: true,
},
{
name: "git with extension",
url: "git://example.com/owner/repo.git",
want: true,
},
{
name: "git+ssh",
url: "git+ssh://git@example.com/owner/repo.git",
want: true,
},
{
name: "https",
url: "https://example.com/owner/repo.git",
want: true,
},
{
name: "git+https",
url: "git+https://example.com/owner/repo.git",
want: true,
},
{
name: "no protocol",
url: "example.com/owner/repo",
@ -121,6 +136,16 @@ func TestParseURL(t *testing.T) {
Path: "/owner/repo.git",
},
},
{
name: "git+https",
url: "git+https://example.com/owner/repo.git",
want: url{
Scheme: "https",
User: "",
Host: "example.com",
Path: "/owner/repo.git",
},
},
{
name: "scp-like",
url: "git@example.com:owner/repo.git",

View file

@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -112,86 +113,101 @@ func runBrowse(opts *BrowseOptions) error {
return fmt.Errorf("unable to determine base repository: %w", err)
}
httpClient, err := opts.HttpClient()
section, err := parseSection(baseRepo, opts)
if err != nil {
return fmt.Errorf("unable to create an http client: %w", err)
}
url := ghrepo.GenerateRepoURL(baseRepo, "")
if opts.SelectorArg == "" {
if opts.ProjectsFlag {
url += "/projects"
}
if opts.SettingsFlag {
url += "/settings"
}
if opts.WikiFlag {
url += "/wiki"
}
if opts.Branch != "" {
url += "/tree/" + opts.Branch + "/"
}
} else {
if isNumber(opts.SelectorArg) {
url += "/issues/" + opts.SelectorArg
} else {
fileArg, err := parseFileArg(opts.SelectorArg)
if err != nil {
return err
}
if opts.Branch != "" {
url += "/tree/" + opts.Branch + "/"
} else {
apiClient := api.NewClientFromHTTP(httpClient)
branchName, err := api.RepoDefaultBranch(apiClient, baseRepo)
if err != nil {
return err
}
url += "/tree/" + branchName + "/"
}
url += fileArg
}
return err
}
url := ghrepo.GenerateRepoURL(baseRepo, section)
if opts.NoBrowserFlag {
fmt.Fprintf(opts.IO.Out, "%s\n", url)
return nil
} else {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "now opening %s in browser\n", url)
}
return opts.Browser.Browse(url)
_, err := fmt.Fprintln(opts.IO.Out, url)
return err
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(url))
}
return opts.Browser.Browse(url)
}
func parseFileArg(fileArg string) (string, error) {
arr := strings.Split(fileArg, ":")
if len(arr) > 2 {
return "", fmt.Errorf("invalid use of colon\nUse 'gh browse --help' for more information about browse\n")
func parseSection(baseRepo ghrepo.Interface, opts *BrowseOptions) (string, error) {
if opts.SelectorArg == "" {
if opts.ProjectsFlag {
return "projects", nil
} else if opts.SettingsFlag {
return "settings", nil
} else if opts.WikiFlag {
return "wiki", nil
} else if opts.Branch == "" {
return "", nil
}
}
if len(arr) > 1 {
out := arr[0] + "#L"
lineRange := strings.Split(arr[1], "-")
if len(lineRange) > 0 {
if !isNumber(lineRange[0]) {
return "", fmt.Errorf("invalid line number after colon\nUse 'gh browse --help' for more information about browse\n")
}
out += lineRange[0]
}
if len(lineRange) > 1 {
if !isNumber(lineRange[1]) {
return "", fmt.Errorf("invalid line range after colon\nUse 'gh browse --help' for more information about browse\n")
}
out += "-L" + lineRange[1]
}
return out, nil
if isNumber(opts.SelectorArg) {
return fmt.Sprintf("issues/%s", opts.SelectorArg), nil
}
return arr[0], nil
filePath, rangeStart, rangeEnd, err := parseFile(opts.SelectorArg)
if err != nil {
return "", err
}
branchName := opts.Branch
if branchName == "" {
httpClient, err := opts.HttpClient()
if err != nil {
return "", err
}
apiClient := api.NewClientFromHTTP(httpClient)
branchName, err = api.RepoDefaultBranch(apiClient, baseRepo)
if err != nil {
return "", fmt.Errorf("error determining the default branch: %w", err)
}
}
if rangeStart > 0 {
var rangeFragment string
if rangeEnd > 0 && rangeStart != rangeEnd {
rangeFragment = fmt.Sprintf("L%d-L%d", rangeStart, rangeEnd)
} else {
rangeFragment = fmt.Sprintf("L%d", rangeStart)
}
return fmt.Sprintf("blob/%s/%s?plain=1#%s", branchName, filePath, rangeFragment), nil
}
return fmt.Sprintf("tree/%s/%s", branchName, filePath), nil
}
func parseFile(f string) (p string, start int, end int, err error) {
parts := strings.SplitN(f, ":", 3)
if len(parts) > 2 {
err = fmt.Errorf("invalid file argument: %q", f)
return
}
p = parts[0]
if len(parts) < 2 {
return
}
if idx := strings.IndexRune(parts[1], '-'); idx >= 0 {
start, err = strconv.Atoi(parts[1][:idx])
if err != nil {
err = fmt.Errorf("invalid file argument: %q", f)
return
}
end, err = strconv.Atoi(parts[1][idx+1:])
if err != nil {
err = fmt.Errorf("invalid file argument: %q", f)
}
return
}
start, err = strconv.Atoi(parts[1])
if err != nil {
err = fmt.Errorf("invalid file argument: %q", f)
}
end = start
return
}
func isNumber(arg string) bool {

View file

@ -213,7 +213,7 @@ func Test_runBrowse(t *testing.T) {
},
baseRepo: ghrepo.New("ravocean", "angur"),
defaultBranch: "trunk",
expectedURL: "https://github.com/ravocean/angur/tree/trunk/path/to/file.txt#L32",
expectedURL: "https://github.com/ravocean/angur/blob/trunk/path/to/file.txt?plain=1#L32",
},
{
name: "file with line range",
@ -222,12 +222,37 @@ func Test_runBrowse(t *testing.T) {
},
baseRepo: ghrepo.New("ravocean", "angur"),
defaultBranch: "trunk",
expectedURL: "https://github.com/ravocean/angur/tree/trunk/path/to/file.txt#L32-L40",
expectedURL: "https://github.com/ravocean/angur/blob/trunk/path/to/file.txt?plain=1#L32-L40",
},
{
name: "invalid default branch",
opts: BrowseOptions{
SelectorArg: "chocolate-pecan-pie.txt",
},
baseRepo: ghrepo.New("andrewhsu", "recipies"),
defaultBranch: "",
wantsErr: true,
},
{
name: "file with invalid line number after colon",
opts: BrowseOptions{
SelectorArg: "laptime-notes.txt:w-9",
},
baseRepo: ghrepo.New("andrewhsu", "sonoma-raceway"),
wantsErr: true,
},
{
name: "file with invalid file format",
opts: BrowseOptions{
SelectorArg: "path/to/file.txt:32:32",
},
baseRepo: ghrepo.New("ttran112", "ttrain211"),
wantsErr: true,
},
{
name: "file with invalid line number",
opts: BrowseOptions{
SelectorArg: "path/to/file.txt:32:32",
SelectorArg: "path/to/file.txt:32a",
},
baseRepo: ghrepo.New("ttran112", "ttrain211"),
wantsErr: true,
@ -258,7 +283,7 @@ func Test_runBrowse(t *testing.T) {
},
baseRepo: ghrepo.New("github", "ThankYouGitHub"),
wantsErr: false,
expectedURL: "https://github.com/github/ThankYouGitHub/tree/first-browse-pull/browse.go#L32",
expectedURL: "https://github.com/github/ThankYouGitHub/blob/first-browse-pull/browse.go?plain=1#L32",
},
{
name: "no browser with branch file and line number",
@ -269,7 +294,7 @@ func Test_runBrowse(t *testing.T) {
},
baseRepo: ghrepo.New("mislav", "will_paginate"),
wantsErr: false,
expectedURL: "https://github.com/mislav/will_paginate/tree/3-0-stable/init.rb#L6",
expectedURL: "https://github.com/mislav/will_paginate/blob/3-0-stable/init.rb?plain=1#L6",
},
}
@ -313,41 +338,3 @@ func Test_runBrowse(t *testing.T) {
})
}
}
func Test_parseFileArg(t *testing.T) {
tests := []struct {
name string
arg string
errorExpected bool
expectedFileArg string
stderrExpected string
}{
{
name: "non line number",
arg: "main.go",
errorExpected: false,
expectedFileArg: "main.go",
},
{
name: "line number",
arg: "main.go:32",
errorExpected: false,
expectedFileArg: "main.go#L32",
},
{
name: "non line number error",
arg: "ma:in.go",
errorExpected: true,
stderrExpected: "invalid line number after colon\nUse 'gh browse --help' for more information about browse\n",
},
}
for _, tt := range tests {
fileArg, err := parseFileArg(tt.arg)
if tt.errorExpected {
assert.Equal(t, err.Error(), tt.stderrExpected)
} else {
assert.Equal(t, err, nil)
assert.Equal(t, tt.expectedFileArg, fileArg)
}
}
}

View file

@ -30,6 +30,8 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
will be forwarded to the %[1]sgh-<extname>%[1]s executable of the extension.
An extension cannot override any of the core gh commands.
See the list of available extensions at <https://github.com/topics/gh-extension>
`, "`"),
Aliases: []string{"extensions"},
}
@ -67,9 +69,25 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
},
},
&cobra.Command{
Use: "install <repo>",
Use: "install <repository>",
Short: "Install a gh extension from a repository",
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
Long: heredoc.Doc(`
Install a GitHub repository locally as a GitHub CLI extension.
The repository argument can be specified in "owner/repo" format as well as a full URL.
The URL format is useful when the repository is not hosted on github.com.
To install an extension in development from the current directory, use "." as the
value of the repository argument.
See the list of available extensions at <https://github.com/topics/gh-extension>
`),
Example: heredoc.Doc(`
$ gh extension install owner/gh-extension
$ gh extension install https://git.example.com/owner/gh-extension
$ gh extension install .
`),
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()
@ -83,16 +101,12 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
if err != nil {
return err
}
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
return err
}
cfg, err := f.Config()
if err != nil {
return err
}
protocol, _ := cfg.Get(repo.RepoHost(), "git_protocol")
return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
return m.Install(repo)
},
},
func() *cobra.Command {

View file

@ -3,14 +3,17 @@ package extension
import (
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
@ -39,13 +42,13 @@ func TestNewCmdExtension(t *testing.T) {
em.ListFunc = func(bool) []extensions.Extension {
return []extensions.Extension{}
}
em.InstallFunc = func(s string, out, errOut io.Writer) error {
em.InstallFunc = func(_ ghrepo.Interface) error {
return nil
}
return func(t *testing.T) {
installCalls := em.InstallCalls()
assert.Equal(t, 1, len(installCalls))
assert.Equal(t, "https://github.com/owner/gh-some-ext.git", installCalls[0].URL)
assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName())
listCalls := em.ListCalls()
assert.Equal(t, 1, len(listCalls))
}
@ -281,12 +284,19 @@ func TestNewCmdExtension(t *testing.T) {
assertFunc = tt.managerStubs(em)
}
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
f := cmdutil.Factory{
Config: func() (config.Config, error) {
return config.NewBlankConfig(), nil
},
IOStreams: ios,
ExtensionManager: em,
HttpClient: func() (*http.Client, error) {
return &client, nil
},
}
cmd := NewCmdExtension(&f)

114
pkg/cmd/extension/http.go Normal file
View file

@ -0,0 +1,114 @@
package extension
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
)
func hasScript(httpClient *http.Client, repo ghrepo.Interface) (hs bool, err error) {
path := fmt.Sprintf("repos/%s/%s/contents/%s",
repo.RepoOwner(), repo.RepoName(), repo.RepoName())
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
resp, err := httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return
}
if resp.StatusCode > 299 {
err = api.HandleHTTPError(resp)
return
}
hs = true
return
}
type releaseAsset struct {
Name string
APIURL string `json:"url"`
}
type release struct {
Tag string `json:"tag_name"`
Assets []releaseAsset
}
// downloadAsset downloads a single asset to the given file path.
func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string) error {
req, err := http.NewRequest("GET", asset.APIURL, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/octet-stream")
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return api.HandleHTTPError(resp)
}
f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
// fetchLatestRelease finds the latest published release for a repository.
func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) {
path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var r release
err = json.Unmarshal(b, &r)
if err != nil {
return nil, err
}
return &r, nil
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
@ -14,10 +15,14 @@ import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/findsh"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/safeexec"
"gopkg.in/yaml.v3"
)
type Manager struct {
@ -25,17 +30,32 @@ type Manager struct {
lookPath func(string) (string, error)
findSh func() (string, error)
newCommand func(string, ...string) *exec.Cmd
platform func() string
client *http.Client
config config.Config
io *iostreams.IOStreams
}
func NewManager() *Manager {
func NewManager(io *iostreams.IOStreams) *Manager {
return &Manager{
dataDir: config.DataDir,
lookPath: safeexec.LookPath,
findSh: findsh.Find,
newCommand: exec.Command,
platform: func() string {
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
},
}
}
func (m *Manager) SetConfig(cfg config.Config) {
m.config = cfg
}
func (m *Manager) SetClient(client *http.Client) {
m.client = client
}
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")
@ -172,7 +192,104 @@ func (m *Manager) InstallLocal(dir string) error {
return makeSymlink(dir, targetLink)
}
func (m *Manager) Install(cloneURL string, stdout, stderr io.Writer) error {
type binManifest struct {
Owner string
Name string
Host string
Tag string
// TODO I may end up not using this; just thinking ahead to local installs
Path string
}
func (m *Manager) Install(repo ghrepo.Interface) error {
isBin, err := isBinExtension(m.client, repo)
if err != nil {
return fmt.Errorf("could not check for binary extension: %w", err)
}
if isBin {
return m.installBin(repo)
}
hs, err := hasScript(m.client, repo)
if err != nil {
return err
}
if !hs {
// TODO open an issue hint, here?
return errors.New("extension is uninstallable: missing executable")
}
protocol, _ := m.config.Get(repo.RepoHost(), "git_protocol")
return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut)
}
func (m *Manager) installBin(repo ghrepo.Interface) error {
var r *release
r, err := fetchLatestRelease(m.client, repo)
if err != nil {
return err
}
suffix := m.platform()
var asset *releaseAsset
for _, a := range r.Assets {
if strings.HasSuffix(a.Name, suffix) {
asset = &a
break
}
}
if asset == nil {
return fmt.Errorf("%s unsupported for %s. Open an issue: `gh issue create -R %s/%s -t'Support %s'`",
repo.RepoName(),
suffix, repo.RepoOwner(), repo.RepoName(), suffix)
}
name := repo.RepoName()
targetDir := filepath.Join(m.installDir(), name)
// TODO clean this up if function errs?
err = os.MkdirAll(targetDir, 0755)
if err != nil {
return fmt.Errorf("failed to create installation directory: %w", err)
}
binPath := filepath.Join(targetDir, name)
err = downloadAsset(m.client, *asset, binPath)
if err != nil {
return fmt.Errorf("failed to download asset %s: %w", asset.Name, err)
}
manifest := binManifest{
Name: name,
Owner: repo.RepoOwner(),
Host: repo.RepoHost(),
Path: binPath,
Tag: r.Tag,
}
bs, err := yaml.Marshal(manifest)
if err != nil {
return fmt.Errorf("failed to serialize manifest: %w", err)
}
manifestPath := filepath.Join(targetDir, "manifest.yml")
f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open manifest for writing: %w", err)
}
defer f.Close()
_, err = f.Write(bs)
if err != nil {
return fmt.Errorf("failed write manifest file: %w", err)
}
return nil
}
func (m *Manager) installGit(cloneURL string, stdout, stderr io.Writer) error {
exe, err := m.lookPath("git")
if err != nil {
return err
@ -346,3 +463,77 @@ func readPathFromFile(path string) (string, error) {
n, err := f.Read(b)
return strings.TrimSpace(string(b[:n])), err
}
func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err error) {
var r *release
r, err = fetchLatestRelease(client, repo)
if err != nil {
httpErr, ok := err.(api.HTTPError)
if ok && httpErr.StatusCode == 404 {
err = nil
return
}
return
}
for _, a := range r.Assets {
dists := possibleDists()
for _, d := range dists {
if strings.HasSuffix(a.Name, d) {
isBin = true
break
}
}
}
return
}
func possibleDists() []string {
return []string{
"aix-ppc64",
"android-386",
"android-amd64",
"android-arm",
"android-arm64",
"darwin-amd64",
"darwin-arm64",
"dragonfly-amd64",
"freebsd-386",
"freebsd-amd64",
"freebsd-arm",
"freebsd-arm64",
"illumos-amd64",
"ios-amd64",
"ios-arm64",
"js-wasm",
"linux-386",
"linux-amd64",
"linux-arm",
"linux-arm64",
"linux-mips",
"linux-mips64",
"linux-mips64le",
"linux-mipsle",
"linux-ppc64",
"linux-ppc64le",
"linux-riscv64",
"linux-s390x",
"netbsd-386",
"netbsd-amd64",
"netbsd-arm",
"netbsd-arm64",
"openbsd-386",
"openbsd-amd64",
"openbsd-arm",
"openbsd-arm64",
"openbsd-mips64",
"plan9-386",
"plan9-amd64",
"plan9-arm",
"solaris-amd64",
"windows-386",
"windows-amd64",
"windows-arm",
}
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -11,7 +12,12 @@ import (
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func TestHelperProcess(t *testing.T) {
@ -28,7 +34,7 @@ func TestHelperProcess(t *testing.T) {
os.Exit(0)
}
func newTestManager(dir string) *Manager {
func newTestManager(dir string, client *http.Client, io *iostreams.IOStreams) *Manager {
return &Manager{
dataDir: func() string { return dir },
lookPath: func(exe string) (string, error) { return exe, nil },
@ -39,6 +45,12 @@ func newTestManager(dir string) *Manager {
cmd.Env = []string{"GH_WANT_HELPER_PROCESS=1"}
return cmd
},
config: config.NewBlankConfig(),
io: io,
client: client,
platform: func() string {
return "windows-amd64"
},
}
}
@ -47,7 +59,7 @@ func TestManager_List(t *testing.T) {
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
exts := m.List(false)
assert.Equal(t, 2, len(exts))
assert.Equal(t, "hello", exts[0].Name())
@ -59,7 +71,7 @@ func TestManager_Dispatch(t *testing.T) {
extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")
assert.NoError(t, stubExtension(extPath))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -80,7 +92,7 @@ func TestManager_Remove(t *testing.T) {
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
err := m.Remove("hello")
assert.NoError(t, err)
@ -96,7 +108,7 @@ func TestManager_Upgrade_AllExtensions(t *testing.T) {
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -121,7 +133,7 @@ func TestManager_Upgrade_RemoteExtension(t *testing.T) {
tempDir := t.TempDir()
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -141,7 +153,7 @@ func TestManager_Upgrade_LocalExtension(t *testing.T) {
tempDir := t.TempDir()
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -158,7 +170,7 @@ func TestManager_Upgrade_Force(t *testing.T) {
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -180,7 +192,7 @@ func TestManager_Upgrade_Force(t *testing.T) {
func TestManager_Upgrade_NoExtensions(t *testing.T) {
tempDir := t.TempDir()
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -190,24 +202,153 @@ func TestManager_Upgrade_NoExtensions(t *testing.T) {
assert.Equal(t, "", stderr.String())
}
func TestManager_Install(t *testing.T) {
func TestManager_Install_git(t *testing.T) {
tempDir := t.TempDir()
m := newTestManager(tempDir)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
err := m.Install("https://github.com/owner/gh-some-ext.git", stdout, stderr)
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &client, io)
reg.Register(
httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"),
httpmock.JSONResponse(
release{
Assets: []releaseAsset{
{
Name: "not-a-binary",
APIURL: "https://example.com/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "repos/owner/gh-some-ext/contents/gh-some-ext"),
httpmock.StringResponse("script"))
repo := ghrepo.New("owner", "gh-some-ext")
err := m.Install(repo)
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("[git clone https://github.com/owner/gh-some-ext.git %s]\n", filepath.Join(tempDir, "extensions", "gh-some-ext")), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Install_binary_unsupported(t *testing.T) {
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
release{
Assets: []releaseAsset{
{
Name: "gh-bin-ext-linux-amd64",
APIURL: "https://example.com/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
release{
Tag: "v1.0.1",
Assets: []releaseAsset{
{
Name: "gh-bin-ext-linux-amd64",
APIURL: "https://example.com/release/cool",
},
},
}))
io, _, _, _ := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
err := m.Install(repo)
assert.Error(t, err)
errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`"
assert.Equal(t, errText, err.Error())
}
func TestManager_Install_binary(t *testing.T) {
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
release{
Assets: []releaseAsset{
{
Name: "gh-bin-ext-windows-amd64",
APIURL: "https://example.com/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
release{
Tag: "v1.0.1",
Assets: []releaseAsset{
{
Name: "gh-bin-ext-windows-amd64",
APIURL: "https://example.com/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "release/cool"),
httpmock.StringResponse("FAKE BINARY"))
io, _, _, _ := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
err := m.Install(repo)
assert.NoError(t, err)
manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/manifest.yml"))
assert.NoError(t, err)
var bm binManifest
err = yaml.Unmarshal(manifest, &bm)
assert.NoError(t, err)
assert.Equal(t, binManifest{
Name: "gh-bin-ext",
Owner: "owner",
Host: "example.com",
Tag: "v1.0.1",
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"),
}, bm)
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"))
assert.NoError(t, err)
assert.Equal(t, "FAKE BINARY", string(fakeBin))
}
func TestManager_Create(t *testing.T) {
tempDir := t.TempDir()
oldWd, _ := os.Getwd()
assert.NoError(t, os.Chdir(tempDir))
t.Cleanup(func() { _ = os.Chdir(oldWd) })
m := newTestManager(tempDir)
m := newTestManager(tempDir, nil, nil)
err := m.Create("gh-test")
assert.NoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))

View file

@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/context"
@ -31,6 +32,7 @@ func New(appVersion string) *cmdutil.Factory {
f.Remotes = remotesFunc(f) // Depends on Config
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
f.Browser = browser(f) // Depends on Config, and IOStreams
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
return f
}
@ -187,6 +189,25 @@ func branchFunc() func() (string, error) {
}
}
func extensionManager(f *cmdutil.Factory) *extension.Manager {
em := extension.NewManager(f.IOStreams)
cfg, err := f.Config()
if err != nil {
return em
}
em.SetConfig(cfg)
client, err := f.HttpClient()
if err != nil {
return em
}
em.SetClient(api.NewCachedClient(client, time.Second*30))
return em
}
func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
io := iostreams.System()
cfg, err := f.Config()

View file

@ -171,7 +171,7 @@ func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.Fi
"query": searchQuery,
}
ic := api.IssuesAndTotalCount{}
ic := api.IssuesAndTotalCount{SearchCapped: limit > 1000}
loop:
for {

View file

@ -166,6 +166,9 @@ func listRun(opts *ListOptions) error {
return opts.Exporter.Write(opts.IO, listResult.Issues)
}
if listResult.SearchCapped {
fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum")
}
if isTerminal {
title := prShared.ListHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, !filterOptions.IsDefault())
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)

View file

@ -10,8 +10,12 @@ import (
"github.com/cli/cli/v2/pkg/githubsearch"
)
func shouldUseSearch(filters prShared.FilterOptions) bool {
return filters.Draft != "" || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0
}
func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
if filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0 {
if shouldUseSearch(filters) {
return searchPullRequests(httpClient, repo, filters, limit)
}
@ -177,12 +181,16 @@ func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters
q.SetBaseBranch(filters.BaseBranch)
}
if filters.Draft != "" {
q.SetDraft(filters.Draft)
}
pageLimit := min(limit, 100)
variables := map[string]interface{}{
"q": q.String(),
}
res := api.PullRequestAndTotalCount{}
res := api.PullRequestAndTotalCount{SearchCapped: limit > 1000}
var check = make(map[int]struct{})
client := api.NewClientFromHTTP(httpClient)

View file

@ -37,6 +37,7 @@ type ListOptions struct {
Author string
Assignee string
Search string
Draft string
}
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
@ -46,6 +47,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
Browser: f.Browser,
}
var draft bool
cmd := &cobra.Command{
Use: "list",
Short: "List and filter pull requests in this repository",
@ -74,6 +77,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
return &cmdutil.FlagError{Err: fmt.Errorf("invalid value for --limit: %v", opts.LimitResults)}
}
if cmd.Flags().Changed("draft") {
opts.Draft = strconv.FormatBool(draft)
}
if runF != nil {
return runF(opts)
}
@ -92,6 +99,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`")
cmd.Flags().BoolVarP(&draft, "draft", "d", false, "Filter by draft state")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields)
return cmd
@ -132,12 +141,12 @@ func listRun(opts *ListOptions) error {
Labels: opts.Labels,
BaseBranch: opts.BaseBranch,
Search: opts.Search,
Draft: opts.Draft,
Fields: defaultFields,
}
if opts.Exporter != nil {
filters.Fields = opts.Exporter.Fields()
}
if opts.WebMode {
prListURL := ghrepo.GenerateRepoURL(baseRepo, "pulls")
openURL, err := shared.ListURLWithQuery(prListURL, filters)
@ -166,6 +175,9 @@ func listRun(opts *ListOptions) error {
return opts.Exporter.Write(opts.IO, listResult.PullRequests)
}
if listResult.SearchCapped {
fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum")
}
if opts.IO.IsStdoutTTY() {
title := shared.ListHeader(ghrepo.FullName(baseRepo), "pull request", len(listResult.PullRequests), listResult.TotalCount, !filters.IsDefault())
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)

View file

@ -176,39 +176,89 @@ func TestPRList_filteringAssignee(t *testing.T) {
}
}
func TestPRList_filteringAssigneeLabels(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
func TestPRList_filteringDraft(t *testing.T) {
tests := []struct {
name string
cli string
expectedQuery string
}{
{
name: "draft",
cli: "--draft",
expectedQuery: `repo:OWNER/REPO is:pr is:open draft:true`,
},
{
name: "non-draft",
cli: "--draft=false",
expectedQuery: `repo:OWNER/REPO is:pr is:open draft:false`,
},
}
_, err := runCommand(http, true, `-l one,two -a hubot`)
if err == nil && err.Error() != "multiple labels with --assignee are not supported" {
t.Fatal(err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query PullRequestSearch\b`),
httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
assert.Equal(t, test.expectedQuery, params["q"].(string))
}))
_, err := runCommand(http, true, test.cli)
if err != nil {
t.Fatal(err)
}
})
}
}
func TestPRList_withInvalidLimitFlag(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
_, err := runCommand(http, true, `--limit=0`)
if err == nil && err.Error() != "invalid limit: 0" {
t.Errorf("error running command `issue list`: %v", err)
}
assert.EqualError(t, err, "invalid value for --limit: 0")
}
func TestPRList_web(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
output, err := runCommand(http, true, "--web -a peter -l bug -l docs -L 10 -s merged -B trunk")
if err != nil {
t.Errorf("error running command `pr list` with `--web` flag: %v", err)
tests := []struct {
name string
cli string
expectedBrowserURL string
}{
{
name: "filters",
cli: "-a peter -l bug -l docs -L 10 -s merged -B trunk",
expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Amerged+assignee%3Apeter+label%3Abug+label%3Adocs+base%3Atrunk",
},
{
name: "draft",
cli: "--draft=true",
expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Aopen+draft%3Atrue",
},
{
name: "non-draft",
cli: "--draft=0",
expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Aopen+draft%3Afalse",
},
}
assert.Equal(t, "", output.String())
assert.Equal(t, "Opening github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr())
assert.Equal(t, "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Amerged+assignee%3Apeter+label%3Abug+label%3Adocs+base%3Atrunk", output.BrowsedURL)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
output, err := runCommand(http, true, "--web "+test.cli)
if err != nil {
t.Errorf("error running command `pr list` with `--web` flag: %v", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, "Opening github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr())
assert.Equal(t, test.expectedBrowserURL, output.BrowsedURL)
})
}
}

View file

@ -157,8 +157,8 @@ type FilterOptions struct {
Mention string
Milestone string
Search string
Fields []string
Draft string
Fields []string
}
func (opts *FilterOptions) IsDefault() bool {
@ -241,7 +241,9 @@ func SearchQueryBuild(options FilterOptions) string {
if options.Search != "" {
q.AddQuery(options.Search)
}
if options.Draft != "" {
q.SetDraft(options.Draft)
}
return q.String()
}

View file

@ -16,6 +16,7 @@ func Test_listURLWithQuery(t *testing.T) {
listURL string
options FilterOptions
}
tests := []struct {
name string
args args
@ -34,6 +35,32 @@ func Test_listURLWithQuery(t *testing.T) {
want: "https://example.com/path?a=b&q=is%3Aissue+is%3Aopen",
wantErr: false,
},
{
name: "draft",
args: args{
listURL: "https://example.com/path",
options: FilterOptions{
Entity: "pr",
State: "open",
Draft: "true",
},
},
want: "https://example.com/path?q=is%3Apr+is%3Aopen+draft%3Atrue",
wantErr: false,
},
{
name: "non-draft",
args: args{
listURL: "https://example.com/path",
options: FilterOptions{
Entity: "pr",
State: "open",
Draft: "false",
},
},
want: "https://example.com/path?q=is%3Apr+is%3Aopen+draft%3Afalse",
wantErr: false,
},
{
name: "all",
args: args{

View file

@ -251,7 +251,7 @@ func createRun(opts *CreateOptions) error {
if !isVisibilityPassed {
newVisibility, err := getVisibility()
if err != nil {
return nil
return err
}
visibility = newVisibility
}

View file

@ -171,6 +171,9 @@ func listRun(opts *ListOptions) error {
tp.EndRow()
}
if listResult.FromSearch && opts.Limit > 1000 {
fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum")
}
if opts.IO.IsStdoutTTY() {
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Language != "" || filter.Topic != ""
title := listHeader(listResult.Owner, len(listResult.Repositories), listResult.TotalCount, hasFilters)

View file

@ -5,6 +5,7 @@ import (
"strings"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/pkg/iostreams"
)
type gitClient interface {
@ -20,7 +21,9 @@ type gitClient interface {
ResetHard(string) error
}
type gitExecuter struct{}
type gitExecuter struct {
io *iostreams.IOStreams
}
func (g *gitExecuter) BranchRemote(branch string) (string, error) {
args := []string{"rev-parse", "--symbolic-full-name", "--abbrev-ref", fmt.Sprintf("%s@{u}", branch)}
@ -64,11 +67,14 @@ func (g *gitExecuter) CurrentBranch() (string, error) {
}
func (g *gitExecuter) Fetch(remote, ref string) error {
args := []string{"fetch", remote, ref}
args := []string{"fetch", "-q", remote, ref}
cmd, err := git.GitCommand(args...)
if err != nil {
return err
}
cmd.Stdin = g.io.In
cmd.Stdout = g.io.Out
cmd.Stderr = g.io.ErrOut
return cmd.Run()
}

View file

@ -34,7 +34,7 @@ func NewCmdSync(f *cmdutil.Factory, runF func(*SyncOptions) error) *cobra.Comman
IO: f.IOStreams,
BaseRepo: f.BaseRepo,
Remotes: f.Remotes,
Git: &gitExecuter{},
Git: &gitExecuter{io: f.IOStreams},
}
cmd := &cobra.Command{
@ -134,6 +134,11 @@ func syncLocalRepo(opts *SyncOptions) error {
}
}
// Git fetch might require input from user, so do it before starting progress indicator.
if err := opts.Git.Fetch(remote, fmt.Sprintf("refs/heads/%s", opts.Branch)); err != nil {
return err
}
opts.IO.StartProgressIndicator()
err = executeLocalRepoSync(srcRepo, remote, opts)
opts.IO.StopProgressIndicator()
@ -232,10 +237,6 @@ func executeLocalRepoSync(srcRepo ghrepo.Interface, remote string, opts *SyncOpt
branch := opts.Branch
useForce := opts.Force
if err := git.Fetch(remote, fmt.Sprintf("refs/heads/%s", branch)); err != nil {
return err
}
hasLocalBranch := git.HasLocalBranch(branch)
if hasLocalBranch {
branchRemote, err := git.BranchRemote(branch)

View file

@ -2,6 +2,8 @@ package extensions
import (
"io"
"github.com/cli/cli/v2/internal/ghrepo"
)
//go:generate moq -rm -out extension_mock.go . Extension
@ -16,7 +18,7 @@ type Extension interface {
//go:generate moq -rm -out manager_mock.go . ExtensionManager
type ExtensionManager interface {
List(includeMetadata bool) []Extension
Install(url string, stdout, stderr io.Writer) error
Install(ghrepo.Interface) error
InstallLocal(dir string) error
Upgrade(name string, force bool, stdout, stderr io.Writer) error
Remove(name string) error

View file

@ -4,6 +4,7 @@
package extensions
import (
"github.com/cli/cli/v2/internal/ghrepo"
"io"
"sync"
)
@ -24,7 +25,7 @@ var _ ExtensionManager = &ExtensionManagerMock{}
// DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) {
// panic("mock out the Dispatch method")
// },
// InstallFunc: func(url string, stdout io.Writer, stderr io.Writer) error {
// InstallFunc: func(interfaceMoqParam ghrepo.Interface) error {
// panic("mock out the Install method")
// },
// InstallLocalFunc: func(dir string) error {
@ -53,7 +54,7 @@ type ExtensionManagerMock struct {
DispatchFunc func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error)
// InstallFunc mocks the Install method.
InstallFunc func(url string, stdout io.Writer, stderr io.Writer) error
InstallFunc func(interfaceMoqParam ghrepo.Interface) error
// InstallLocalFunc mocks the InstallLocal method.
InstallLocalFunc func(dir string) error
@ -87,12 +88,8 @@ type ExtensionManagerMock struct {
}
// Install holds details about calls to the Install method.
Install []struct {
// URL is the url argument value.
URL string
// Stdout is the stdout argument value.
Stdout io.Writer
// Stderr is the stderr argument value.
Stderr io.Writer
// InterfaceMoqParam is the interfaceMoqParam argument value.
InterfaceMoqParam ghrepo.Interface
}
// InstallLocal holds details about calls to the InstallLocal method.
InstallLocal []struct {
@ -205,37 +202,29 @@ func (mock *ExtensionManagerMock) DispatchCalls() []struct {
}
// Install calls InstallFunc.
func (mock *ExtensionManagerMock) Install(url string, stdout io.Writer, stderr io.Writer) error {
func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface) error {
if mock.InstallFunc == nil {
panic("ExtensionManagerMock.InstallFunc: method is nil but ExtensionManager.Install was just called")
}
callInfo := struct {
URL string
Stdout io.Writer
Stderr io.Writer
InterfaceMoqParam ghrepo.Interface
}{
URL: url,
Stdout: stdout,
Stderr: stderr,
InterfaceMoqParam: interfaceMoqParam,
}
mock.lockInstall.Lock()
mock.calls.Install = append(mock.calls.Install, callInfo)
mock.lockInstall.Unlock()
return mock.InstallFunc(url, stdout, stderr)
return mock.InstallFunc(interfaceMoqParam)
}
// InstallCalls gets all the calls that were made to Install.
// Check the length with:
// len(mockedExtensionManager.InstallCalls())
func (mock *ExtensionManagerMock) InstallCalls() []struct {
URL string
Stdout io.Writer
Stderr io.Writer
InterfaceMoqParam ghrepo.Interface
} {
var calls []struct {
URL string
Stdout io.Writer
Stderr io.Writer
InterfaceMoqParam ghrepo.Interface
}
mock.lockInstall.RLock()
calls = mock.calls.Install

View file

@ -54,6 +54,7 @@ type Query struct {
forkState string
visibility string
isArchived *bool
draft string
}
func (q *Query) InRepository(nameWithOwner string) {
@ -139,6 +140,10 @@ func (q *Query) SetArchived(isArchived bool) {
q.isArchived = &isArchived
}
func (q *Query) SetDraft(draft string) {
q.draft = draft
}
func (q *Query) String() string {
var qs string
@ -198,6 +203,9 @@ func (q *Query) String() string {
if q.headBranch != "" {
qs += fmt.Sprintf("head:%s ", quote(q.headBranch))
}
if q.draft != "" {
qs += fmt.Sprintf("draft:%v ", q.draft)
}
if q.sort != "" {
qs += fmt.Sprintf("sort:%s ", q.sort)