Merge branch 'master' into glamour-markdown

This commit is contained in:
Nate Smith 2020-02-18 14:23:49 -06:00 committed by GitHub
commit c65ac4ae9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 846 additions and 457 deletions

View file

@ -26,6 +26,8 @@ Contributions to this project are [released][legal] to the public under the [pro
Please note that this project adheres to a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
We generate manual pages from source on every release! You do not need to submit PRs for those specifically; the docs will get updated if your PR gets accepted.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)

View file

@ -1,6 +1,9 @@
---
name: "\U0001F41B Bug report"
about: Report a bug or unexpected behavior while using GitHub CLI
title: ''
labels: bug
assignees: ''
---

View file

@ -1,6 +1,9 @@
---
name: "\U00002B50 Submit a request"
name: " Submit a request"
about: Surface a feature or problem that you think should be solved
title: ''
labels: enhancement
assignees: ''
---

View file

@ -0,0 +1,13 @@
<!--
Please make sure you read our contributing guidelines at
https://github.com/cli/cli/blob/master/.github/CONTRIBUTING.md
before opening opening a pull request. Thanks!
-->
## Summary
closes #[issue number]
## Details
-

View file

@ -1,5 +1,5 @@
name: Tests
on: [push]
on: [push, pull_request]
jobs:
build:
strategy:

View file

@ -25,8 +25,6 @@ jobs:
version: latest
args: release --release-notes=CHANGELOG.md
env:
GH_OAUTH_CLIENT_ID: 178c6fc778ccc68e1d6a
GH_OAUTH_CLIENT_SECRET: ${{secrets.OAUTH_CLIENT_SECRET}}
GITHUB_TOKEN: ${{secrets.UPLOAD_GITHUB_TOKEN}}
msi:
needs: goreleaser

6
.gitignore vendored
View file

@ -5,3 +5,9 @@
/site
.github/**/node_modules
/CHANGELOG.md
# VS Code
.vscode
# macOS
.DS_Store

View file

@ -13,8 +13,6 @@ builds:
main: ./cmd/gh
ldflags:
- -s -w -X github.com/cli/cli/command.Version={{.Version}} -X github.com/cli/cli/command.BuildDate={{time "2006-01-02"}}
- -X github.com/cli/cli/context.oauthClientID={{.Env.GH_OAUTH_CLIENT_ID}}
- -X github.com/cli/cli/context.oauthClientSecret={{.Env.GH_OAUTH_CLIENT_SECRET}}
- -X main.updaterEnabled=cli/cli
id: macos
goos: [darwin]
@ -87,5 +85,6 @@ scoop:
name: vilmibm
email: vilmibm@github.com
homepage: https://github.com/cli/cli
skip_upload: auto
description: GitHub CLI
license: MIT

View file

@ -2,15 +2,21 @@ BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\
{{end}}' ./...)
GH_VERSION ?= $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD)
DATE_FMT = +%Y-%m-%d
ifdef SOURCE_DATE_EPOCH
BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)")
else
BUILD_DATE ?= $(shell date "$(DATE_FMT)")
endif
LDFLAGS := -X github.com/cli/cli/command.Version=$(GH_VERSION) $(LDFLAGS)
LDFLAGS := -X github.com/cli/cli/command.BuildDate=$(shell date +%Y-%m-%d) $(LDFLAGS)
LDFLAGS := -X github.com/cli/cli/command.BuildDate=$(BUILD_DATE) $(LDFLAGS)
ifdef GH_OAUTH_CLIENT_SECRET
LDFLAGS := -X github.com/cli/cli/context.oauthClientID=$(GH_OAUTH_CLIENT_ID) $(LDFLAGS)
LDFLAGS := -X github.com/cli/cli/context.oauthClientSecret=$(GH_OAUTH_CLIENT_SECRET) $(LDFLAGS)
endif
bin/gh: $(BUILD_FILES)
@go build -ldflags "$(LDFLAGS)" -o "$@" ./cmd/gh
@go build -trimpath -ldflags "$(LDFLAGS)" -o "$@" ./cmd/gh
test:
go test ./...

View file

@ -5,6 +5,10 @@ the terminal next to where you are already working with `git` and your code.
![screenshot](https://user-images.githubusercontent.com/98482/73286699-9f922180-41bd-11ea-87c9-60a2d31fd0ac.png)
## Availability
While in beta, GitHub CLI is available for repos hosted on GitHub.com only. It does not currently support repositories hosted on GitHub Enterprise Server or other hosting providers.
## We need your feedback
GitHub CLI is currently early in its development, and we're hoping to get feedback from people using it.
@ -40,7 +44,9 @@ Upgrade: `brew update && brew upgrade gh`
### Windows
`gh` is available via [scoop][]:
`gh` is available via [scoop][], [Chocolatey][], and as downloadable MSI.
#### scoop
Install:
@ -51,7 +57,23 @@ scoop install gh
Upgrade: `scoop update gh`
Signed MSI installers are also available on the [releases page][].
#### Chocolatey
Install:
```
choco install gh
```
Upgrade:
```
choco upgrade gh
```
#### Signed MSI
MSI installers are available for download on the [releases page][].
### Debian/Ubuntu Linux
@ -67,15 +89,30 @@ Install and upgrade:
1. Download the `.rpm` file from the [releases page][]
2. `sudo yum localinstall gh_*_linux_amd64.rpm` install the downloaded file
### openSUSE/SUSE Linux
Install and upgrade:
1. Download the `.rpm` file from the [releases page][]
2. `sudo zypper in gh_*_linux_amd64.rpm` install the downloaded file
### Arch Linux
Arch Linux users can install from the AUR: https://aur.archlinux.org/packages/github-cli/
```bash
$ yay -S github-cli
```
### Other platforms
Install a prebuilt binary from the [releases page][]
### [Build from source](/source.md)
<!-- TODO eventually we'll have https://cli.github.com/manual -->
[docs]: https://cli.github.io/cli/gh
[docs]: https://cli.github.com/manual
[scoop]: https://scoop.sh
[Chocolatey]: https://chocolatey.org
[releases page]: https://github.com/cli/cli/releases/latest
[hub]: https://github.com/github/hub
[contributing page]: https://github.com/cli/cli/blob/master/.github/CONTRIBUTING.md

View file

@ -1,6 +1,7 @@
package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
@ -35,3 +36,23 @@ func (f *FakeHTTP) RoundTrip(req *http.Request) (*http.Response, error) {
f.Requests = append(f.Requests, req)
return resp, nil
}
func (f *FakeHTTP) StubRepoResponse(owner, repo string) {
body := bytes.NewBufferString(fmt.Sprintf(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "WRITE"
} } }
`, repo, owner))
resp := &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(body),
}
f.responseStubs = append(f.responseStubs, resp)
}

View file

@ -2,6 +2,7 @@ package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
@ -20,7 +21,7 @@ func randomString(length int) (string, error) {
if err != nil {
return "", err
}
return fmt.Sprintf("%x", b), nil
return hex.EncodeToString(b), nil
}
// OAuthFlow represents the setup for authenticating with GitHub

View file

@ -83,7 +83,7 @@ func issueList(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := ctx.BaseRepo()
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
@ -108,9 +108,9 @@ func issueList(cmd *cobra.Command, args []string) error {
return err
}
fmt.Fprintf(colorableErr(cmd), "\nIssues for %s\n\n", ghrepo.FullName(baseRepo))
fmt.Fprintf(colorableErr(cmd), "\nIssues for %s\n\n", ghrepo.FullName(*baseRepo))
issues, err := api.IssueList(apiClient, baseRepo, state, labels, assignee, limit)
issues, err := api.IssueList(apiClient, *baseRepo, state, labels, assignee, limit)
if err != nil {
return err
}
@ -158,7 +158,7 @@ func issueStatus(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := ctx.BaseRepo()
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
@ -168,7 +168,7 @@ func issueStatus(cmd *cobra.Command, args []string) error {
return err
}
issuePayload, err := api.IssueStatus(apiClient, baseRepo, currentUser)
issuePayload, err := api.IssueStatus(apiClient, *baseRepo, currentUser)
if err != nil {
return err
}
@ -176,7 +176,7 @@ func issueStatus(cmd *cobra.Command, args []string) error {
out := colorableOut(cmd)
fmt.Fprintln(out, "")
fmt.Fprintf(out, "Relevant issues in %s\n", ghrepo.FullName(baseRepo))
fmt.Fprintf(out, "Relevant issues in %s\n", ghrepo.FullName(*baseRepo))
fmt.Fprintln(out, "")
printHeader(out, "Issues assigned to you")
@ -210,16 +210,17 @@ func issueStatus(cmd *cobra.Command, args []string) error {
func issueView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := ctx.BaseRepo()
if err != nil {
return err
}
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
issue, err := issueFromArg(apiClient, baseRepo, args[0])
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
issue, err := issueFromArg(apiClient, *baseRepo, args[0])
if err != nil {
return err
}
@ -253,13 +254,17 @@ func printIssuePreview(out io.Writer, issue *api.Issue) error {
utils.Pluralize(issue.Comments.TotalCount, "comment"),
coloredLabels,
)))
fmt.Fprintln(out)
md, err := utils.RenderMarkdown(issue.Body)
if err != nil {
return err
if issue.Body != "" {
fmt.Fprintln(out)
md, err := utils.RenderMarkdown(issue.Body)
if err != nil {
return err
}
fmt.Fprintln(out, md)
fmt.Fprintln(out)
}
fmt.Fprintln(out, md)
fmt.Fprintln(out)
fmt.Fprintf(out, utils.Gray("View this issue on GitHub: %s\n"), issue.URL)
return nil
}
@ -282,22 +287,30 @@ func issueFromArg(apiClient *api.Client, baseRepo ghrepo.Interface, arg string)
func issueCreate(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := ctx.BaseRepo()
// NB no auto forking like over in pr create
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
fmt.Fprintf(colorableErr(cmd), "\nCreating issue in %s\n\n", ghrepo.FullName(baseRepo))
fmt.Fprintf(colorableErr(cmd), "\nCreating issue in %s\n\n", ghrepo.FullName(*baseRepo))
baseOverride, err := cmd.Flags().GetString("repo")
if err != nil {
return err
}
var templateFiles []string
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "ISSUE_TEMPLATE")
if baseOverride == "" {
if rootDir, err := git.ToplevelDir(); err == nil {
// TODO: figure out how to stub this in tests
templateFiles = githubtemplate.Find(rootDir, "ISSUE_TEMPLATE")
}
}
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
// TODO: move URL generation into GitHubRepository
openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(baseRepo))
openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(*baseRepo))
if len(templateFiles) > 1 {
openURL += "/choose"
}
@ -310,12 +323,12 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return err
}
repo, err := api.GitHubRepo(apiClient, baseRepo)
repo, err := api.GitHubRepo(apiClient, *baseRepo)
if err != nil {
return err
}
if !repo.HasIssuesEnabled {
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(*baseRepo))
}
action := SubmitAction
@ -356,7 +369,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
if action == PreviewAction {
openURL := fmt.Sprintf(
"https://github.com/%s/issues/new/?title=%s&body=%s",
ghrepo.FullName(baseRepo),
ghrepo.FullName(*baseRepo),
url.QueryEscape(title),
url.QueryEscape(body),
)

View file

@ -15,6 +15,7 @@ import (
func TestIssueStatus(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/issueStatus.json")
defer jsonFile.Close()
@ -43,6 +44,7 @@ func TestIssueStatus(t *testing.T) {
func TestIssueStatus_blankSlate(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -79,6 +81,7 @@ Issues opened by you
func TestIssueStatus_disabledIssues(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -95,6 +98,7 @@ func TestIssueStatus_disabledIssues(t *testing.T) {
func TestIssueList(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/issueList.json")
defer jsonFile.Close()
@ -122,6 +126,7 @@ func TestIssueList(t *testing.T) {
func TestIssueList_withFlags(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -142,7 +147,7 @@ Issues for OWNER/REPO
No issues match your search
`)
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Assignee string
@ -160,6 +165,7 @@ No issues match your search
func TestIssueList_nullAssigneeLabels(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -173,7 +179,7 @@ func TestIssueList_nullAssigneeLabels(t *testing.T) {
t.Errorf("error running command `issue list`: %v", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables map[string]interface{}
}{}
@ -188,6 +194,7 @@ func TestIssueList_nullAssigneeLabels(t *testing.T) {
func TestIssueList_disabledIssues(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -204,6 +211,7 @@ func TestIssueList_disabledIssues(t *testing.T) {
func TestIssueView(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
@ -237,6 +245,7 @@ func TestIssueView(t *testing.T) {
func TestIssueView_preview(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
@ -279,6 +288,51 @@ func TestIssueView_preview(t *testing.T) {
}
}
func TestIssueView_previewWithEmptyBody(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
"number": 123,
"body": "",
"title": "ix of coins",
"author": {
"login": "marseilles"
},
"labels": {
"nodes": [
{"name": "tarot"}
]
},
"comments": {
"totalCount": 9
},
"url": "https://github.com/OWNER/REPO/issues/123"
} } } }
`))
output, err := RunCommand(issueViewCmd, "issue view -p 123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
eq(t, output.Stderr(), "")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`ix of coins`),
regexp.MustCompile(`opened by marseilles. 9 comments. \(tarot\)`),
regexp.MustCompile(`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`),
}
for _, r := range expectedLines {
if !r.MatchString(output.String()) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
return
}
}
}
func TestIssueView_notFound(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
@ -309,6 +363,7 @@ func TestIssueView_notFound(t *testing.T) {
func TestIssueView_disabledIssues(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -326,6 +381,7 @@ func TestIssueView_disabledIssues(t *testing.T) {
func TestIssueView_urlArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
@ -358,6 +414,7 @@ func TestIssueView_urlArg(t *testing.T) {
func TestIssueCreate(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -376,7 +433,7 @@ func TestIssueCreate(t *testing.T) {
t.Errorf("error running command `issue create`: %v", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
@ -398,6 +455,7 @@ func TestIssueCreate(t *testing.T) {
func TestIssueCreate_disabledIssues(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
@ -414,7 +472,8 @@ func TestIssueCreate_disabledIssues(t *testing.T) {
func TestIssueCreate_web(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
initFakeHTTP()
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {

View file

@ -1,11 +1,8 @@
package command
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
@ -46,17 +43,6 @@ A pull request can be supplied as argument in any of the following formats:
- by URL, e.g. "https://github.com/OWNER/REPO/pull/123"; or
- by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".`,
}
var prCheckoutCmd = &cobra.Command{
Use: "checkout {<number> | <url> | <branch>}",
Short: "Check out a pull request in Git",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a PR number as an argument")
}
return nil
},
RunE: prCheckout,
}
var prListCmd = &cobra.Command{
Use: "list",
Short: "List and filter pull requests in this repository",
@ -84,10 +70,6 @@ func prStatus(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := ctx.BaseRepo()
if err != nil {
return err
}
currentPRNumber, currentPRHeadRef, err := prSelectorForCurrentBranch(ctx)
if err != nil {
return err
@ -97,7 +79,12 @@ func prStatus(cmd *cobra.Command, args []string) error {
return err
}
prPayload, err := api.PullRequests(apiClient, baseRepo, currentPRNumber, currentPRHeadRef, currentUser)
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
prPayload, err := api.PullRequests(apiClient, *baseRepo, currentPRNumber, currentPRHeadRef, currentUser)
if err != nil {
return err
}
@ -105,7 +92,7 @@ func prStatus(cmd *cobra.Command, args []string) error {
out := colorableOut(cmd)
fmt.Fprintln(out, "")
fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRepo))
fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(*baseRepo))
fmt.Fprintln(out, "")
printHeader(out, "Current branch")
@ -143,12 +130,12 @@ func prList(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := ctx.BaseRepo()
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
fmt.Fprintf(colorableErr(cmd), "\nPull requests for %s\n\n", ghrepo.FullName(baseRepo))
fmt.Fprintf(colorableErr(cmd), "\nPull requests for %s\n\n", ghrepo.FullName(*baseRepo))
limit, err := cmd.Flags().GetInt("limit")
if err != nil {
@ -186,8 +173,8 @@ func prList(cmd *cobra.Command, args []string) error {
}
params := map[string]interface{}{
"owner": baseRepo.RepoOwner(),
"repo": baseRepo.RepoName(),
"owner": (*baseRepo).RepoOwner(),
"repo": (*baseRepo).RepoName(),
"state": graphqlState,
}
if len(labels) > 0 {
@ -206,7 +193,7 @@ func prList(cmd *cobra.Command, args []string) error {
}
if len(prs) == 0 {
colorErr := colorableErr(cmd) // Send to stderr because otherwise when piping this command it would seem like the "no open prs" message is acually a pr
colorErr := colorableErr(cmd) // Send to stderr because otherwise when piping this command it would seem like the "no open prs" message is actually a pr
msg := "There are no open pull requests"
userSetFlags := false
@ -254,12 +241,13 @@ func colorFuncForState(state string) func(string) string {
func prView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := ctx.BaseRepo()
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
apiClient, err := apiClientForContext(ctx)
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
@ -272,7 +260,7 @@ func prView(cmd *cobra.Command, args []string) error {
var openURL string
var pr *api.PullRequest
if len(args) > 0 {
pr, err = prFromArg(apiClient, baseRepo, args[0])
pr, err = prFromArg(apiClient, *baseRepo, args[0])
if err != nil {
return err
}
@ -284,15 +272,15 @@ func prView(cmd *cobra.Command, args []string) error {
}
if prNumber > 0 {
openURL = fmt.Sprintf("https://github.com/%s/pull/%d", ghrepo.FullName(baseRepo), prNumber)
openURL = fmt.Sprintf("https://github.com/%s/pull/%d", ghrepo.FullName(*baseRepo), prNumber)
if preview {
pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNumber)
pr, err = api.PullRequestByNumber(apiClient, *baseRepo, prNumber)
if err != nil {
return err
}
}
} else {
pr, err = api.PullRequestForBranch(apiClient, baseRepo, branchWithOwner)
pr, err = api.PullRequestForBranch(apiClient, *baseRepo, branchWithOwner)
if err != nil {
return err
}
@ -319,13 +307,16 @@ func printPrPreview(out io.Writer, pr *api.PullRequest) error {
pr.BaseRefName,
pr.HeadRefName,
)))
fmt.Fprintln(out)
md, err := utils.RenderMarkdown(pr.Body)
if err != nil {
return err
if pr.Body != "" {
fmt.Fprintln(out)
md, err := utils.RenderMarkdown(pr.Body)
if err != nil {
return err
}
fmt.Fprintln(out, md)
fmt.Fprintln(out)
}
fmt.Fprintln(out, md)
fmt.Fprintln(out)
fmt.Fprintf(out, utils.Gray("View this pull request on GitHub: %s\n"), pr.URL)
return nil
}
@ -390,95 +381,6 @@ func prSelectorForCurrentBranch(ctx context.Context) (prNumber int, prHeadRef st
return
}
func prCheckout(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
currentBranch, _ := ctx.Branch()
remotes, err := ctx.Remotes()
if err != nil {
return err
}
// FIXME: duplicates logic from fsContext.BaseRepo
baseRemote, err := remotes.FindByName("upstream", "github", "origin", "*")
if err != nil {
return err
}
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
pr, err := prFromArg(apiClient, baseRemote, args[0])
if err != nil {
return err
}
headRemote := baseRemote
if pr.IsCrossRepository {
headRemote, _ = remotes.FindByRepo(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
}
cmdQueue := [][]string{}
newBranchName := pr.HeadRefName
if headRemote != nil {
// there is an existing git remote for PR head
remoteBranch := fmt.Sprintf("%s/%s", headRemote.Name, pr.HeadRefName)
refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", pr.HeadRefName, remoteBranch)
cmdQueue = append(cmdQueue, []string{"git", "fetch", headRemote.Name, refSpec})
// local branch already exists
if git.VerifyRef("refs/heads/" + newBranchName) {
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
} else {
cmdQueue = append(cmdQueue, []string{"git", "checkout", "-b", newBranchName, "--no-track", remoteBranch})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/" + pr.HeadRefName})
}
} else {
// no git remote for PR head
// avoid naming the new branch the same as the default branch
if newBranchName == pr.HeadRepository.DefaultBranchRef.Name {
newBranchName = fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, newBranchName)
}
ref := fmt.Sprintf("refs/pull/%d/head", pr.Number)
if newBranchName == currentBranch {
// PR head matches currently checked out branch
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, ref})
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
} else {
// create a new branch
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName)})
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
}
remote := baseRemote.Name
mergeRef := ref
if pr.MaintainerCanModify {
remote = fmt.Sprintf("https://github.com/%s/%s.git", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef})
}
}
for _, args := range cmdQueue {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := utils.PrepareCmd(cmd).Run(); err != nil {
return err
}
}
return nil
}
func printPrs(w io.Writer, totalCount int, prs ...api.PullRequest) {
for _, pr := range prs {
prNumber := fmt.Sprintf("#%d", pr.Number)
@ -507,11 +409,11 @@ func printPrs(w io.Writer, totalCount int, prs ...api.PullRequest) {
}
if reviews.ChangesRequested {
fmt.Fprintf(w, " - %s", utils.Red("changes requested"))
fmt.Fprintf(w, " - %s", utils.Red("Changes requested"))
} else if reviews.ReviewRequired {
fmt.Fprintf(w, " - %s", utils.Yellow("review required"))
fmt.Fprintf(w, " - %s", utils.Yellow("Review required"))
} else if reviews.Approved {
fmt.Fprintf(w, " - %s", utils.Green("approved"))
fmt.Fprintf(w, " - %s", utils.Green("Approved"))
}
fmt.Fprint(w, "\n")

113
command/pr_checkout.go Normal file
View file

@ -0,0 +1,113 @@
package command
import (
"errors"
"fmt"
"os"
"os/exec"
"github.com/cli/cli/git"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
func prCheckout(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
currentBranch, _ := ctx.Branch()
remotes, err := ctx.Remotes()
if err != nil {
return err
}
// FIXME: duplicates logic from fsContext.BaseRepo
baseRemote, err := remotes.FindByName("upstream", "github", "origin", "*")
if err != nil {
return err
}
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
pr, err := prFromArg(apiClient, baseRemote, args[0])
if err != nil {
return err
}
headRemote := baseRemote
if pr.IsCrossRepository {
headRemote, _ = remotes.FindByRepo(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
}
cmdQueue := [][]string{}
newBranchName := pr.HeadRefName
if headRemote != nil {
// there is an existing git remote for PR head
remoteBranch := fmt.Sprintf("%s/%s", headRemote.Name, pr.HeadRefName)
refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", pr.HeadRefName, remoteBranch)
cmdQueue = append(cmdQueue, []string{"git", "fetch", headRemote.Name, refSpec})
// local branch already exists
if git.VerifyRef("refs/heads/" + newBranchName) {
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
} else {
cmdQueue = append(cmdQueue, []string{"git", "checkout", "-b", newBranchName, "--no-track", remoteBranch})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/" + pr.HeadRefName})
}
} else {
// no git remote for PR head
// avoid naming the new branch the same as the default branch
if newBranchName == pr.HeadRepository.DefaultBranchRef.Name {
newBranchName = fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, newBranchName)
}
ref := fmt.Sprintf("refs/pull/%d/head", pr.Number)
if newBranchName == currentBranch {
// PR head matches currently checked out branch
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, ref})
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
} else {
// create a new branch
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName)})
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
}
remote := baseRemote.Name
mergeRef := ref
if pr.MaintainerCanModify {
remote = fmt.Sprintf("https://github.com/%s/%s.git", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote})
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef})
}
}
for _, args := range cmdQueue {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := utils.PrepareCmd(cmd).Run(); err != nil {
return err
}
}
return nil
}
var prCheckoutCmd = &cobra.Command{
Use: "checkout {<number> | <url> | <branch>}",
Short: "Check out a pull request in Git",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a PR number as an argument")
}
return nil
},
RunE: prCheckout,
}

View file

@ -1,10 +1,8 @@
package command
import (
"errors"
"fmt"
"net/url"
"sort"
"time"
"github.com/cli/cli/api"
@ -29,7 +27,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
baseRepoOverride, _ := cmd.Flags().GetString("repo")
repoContext, err := resolveRemotesToRepos(remotes, client, baseRepoOverride)
repoContext, err := context.ResolveRemotesToRepos(remotes, client, baseRepoOverride)
if err != nil {
return err
}
@ -178,12 +176,17 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("pull request title must not be blank")
}
headRefName := headBranch
if !ghrepo.IsSame(headRemote, baseRepo) {
headRefName = fmt.Sprintf("%s:%s", headRemote.RepoOwner(), headBranch)
}
params := map[string]interface{}{
"title": title,
"body": body,
"draft": isDraft,
"baseRefName": baseBranch,
"headRefName": headBranch,
"headRefName": headRefName,
}
pr, err := api.CreatePullRequest(client, baseRepo, params)
@ -229,97 +232,3 @@ func init() {
"The branch into which you want your code merged")
prCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create a pull request")
}
// cap the number of git remotes looked up, since the user might have an
// unusally large number of git remotes
const maxRemotesForLookup = 5
func resolveRemotesToRepos(remotes context.Remotes, client *api.Client, base string) (resolvedRemotes, error) {
sort.Stable(remotes)
lenRemotesForLookup := len(remotes)
if lenRemotesForLookup > maxRemotesForLookup {
lenRemotesForLookup = maxRemotesForLookup
}
hasBaseOverride := base != ""
baseOverride := ghrepo.FromFullName(base)
foundBaseOverride := false
repos := []ghrepo.Interface{}
for _, r := range remotes[:lenRemotesForLookup] {
repos = append(repos, r)
if ghrepo.IsSame(r, baseOverride) {
foundBaseOverride = true
}
}
if hasBaseOverride && !foundBaseOverride {
// additionally, look up the explicitly specified base repo if it's not
// already covered by git remotes
repos = append(repos, baseOverride)
}
result := resolvedRemotes{remotes: remotes}
if hasBaseOverride {
result.baseOverride = baseOverride
}
networkResult, err := api.RepoNetwork(client, repos)
if err != nil {
return result, err
}
result.network = networkResult
return result, nil
}
type resolvedRemotes struct {
baseOverride ghrepo.Interface
remotes context.Remotes
network api.RepoNetworkResult
}
// BaseRepo is the first found repository in the "upstream", "github", "origin"
// git remote order, resolved to the parent repo if the git remote points to a fork
func (r resolvedRemotes) BaseRepo() (*api.Repository, error) {
if r.baseOverride != nil {
for _, repo := range r.network.Repositories {
if repo != nil && ghrepo.IsSame(repo, r.baseOverride) {
return repo, nil
}
}
return nil, fmt.Errorf("failed looking up information about the '%s' repository",
ghrepo.FullName(r.baseOverride))
}
for _, repo := range r.network.Repositories {
if repo == nil {
continue
}
if repo.IsFork() {
return repo.Parent, nil
}
return repo, nil
}
return nil, errors.New("not found")
}
// HeadRepo is the first found repository that has push access
func (r resolvedRemotes) HeadRepo() (*api.Repository, error) {
for _, repo := range r.network.Repositories {
if repo != nil && repo.ViewerCanPush() {
return repo, nil
}
}
return nil, errors.New("none of the repositories have push access")
}
// RemoteForRepo finds the git remote that points to a repository
func (r resolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*context.Remote, error) {
for i, remote := range r.remotes {
if ghrepo.IsSame(remote, repo) ||
// additionally, look up the resolved repository name in case this
// git remote points to this repository via a redirect
(r.network.Repositories[i] != nil && ghrepo.IsSame(r.network.Repositories[i], repo)) {
return remote, nil
}
}
return nil, errors.New("not found")
}

View file

@ -10,10 +10,8 @@ import (
"strings"
"testing"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/test"
"github.com/cli/cli/utils"
)
@ -43,28 +41,9 @@ func TestPrCreateHelperProcess(*testing.T) {
}
func TestPRCreate(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("feature")
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
initContext = func() context.Context {
return ctx
}
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"defaultBranchRef": {
"name": "master",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "WRITE"
} } }
`))
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
@ -104,28 +83,9 @@ func TestPRCreate(t *testing.T) {
}
func TestPRCreate_web(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("feature")
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
initContext = func() context.Context {
return ctx
}
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"defaultBranchRef": {
"name": "master",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "WRITE"
} } }
`))
http.StubRepoResponse("OWNER", "REPO")
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
@ -146,28 +106,10 @@ func TestPRCreate_web(t *testing.T) {
}
func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("feature")
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
initContext = func() context.Context {
return ctx
}
initBlankContext("OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "REPO",
"owner": {"login": "OWNER"},
"defaultBranchRef": {
"name": "master",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "WRITE"
} } }
`))
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
@ -190,111 +132,85 @@ Creating pull request for feature into master in OWNER/REPO
`)
}
func TestPRCreate_cross_repo_same_branch(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("default")
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
"fork": "MYSELF/REPO",
})
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repo_000": {
"id": "REPOID0",
"name": "REPO",
"owner": {"login": "OWNER"},
"defaultBranchRef": {
"name": "default",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "READ"
},
"repo_001" : {
"parent": {
"id": "REPOID0",
"name": "REPO",
"owner": {"login": "OWNER"},
"defaultBranchRef": {
"name": "default",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "READ"
},
"id": "REPOID1",
"name": "REPO",
"owner": {"login": "MYSELF"},
"defaultBranchRef": {
"name": "default",
"target": {"oid": "deadbeef"}
},
"viewerPermission": "WRITE"
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
func Test_resolvedRemotes_clonedFork(t *testing.T) {
resolved := resolvedRemotes{
baseOverride: nil,
remotes: context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
},
},
network: api.RepoNetworkResult{
Repositories: []*api.Repository{
&api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "OWNER"},
ViewerPermission: "ADMIN",
Parent: &api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "PARENTOWNER"},
ViewerPermission: "READ",
},
},
},
},
}
origGitCommand := git.GitCommand
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "clean")
defer func() {
git.GitCommand = origGitCommand
}()
baseRepo, err := resolved.BaseRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(baseRepo), "PARENTOWNER/REPO")
baseRemote, err := resolved.RemoteForRepo(baseRepo)
if baseRemote != nil || err == nil {
t.Error("did not expect any remote for base")
}
output, err := RunCommand(prCreateCmd, `pr create -t "cross repo" -b "same branch"`)
eq(t, err, nil)
headRepo, err := resolved.HeadRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(headRepo), "OWNER/REPO")
headRemote, err := resolved.RemoteForRepo(headRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if headRemote.Name != "origin" {
t.Errorf("got remote %q", headRemote.Name)
}
}
func Test_resolvedRemotes_triangularSetup(t *testing.T) {
resolved := resolvedRemotes{
baseOverride: nil,
remotes: context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
},
&context.Remote{
Remote: &git.Remote{Name: "fork"},
Owner: "MYSELF",
Repo: "REPO",
},
},
network: api.RepoNetworkResult{
Repositories: []*api.Repository{
&api.Repository{
Name: "NEWNAME",
Owner: api.RepositoryOwner{Login: "NEWOWNER"},
ViewerPermission: "READ",
},
&api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "MYSELF"},
ViewerPermission: "ADMIN",
},
},
},
}
baseRepo, err := resolved.BaseRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(baseRepo), "NEWOWNER/NEWNAME")
baseRemote, err := resolved.RemoteForRepo(baseRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if baseRemote.Name != "origin" {
t.Errorf("got remote %q", baseRemote.Name)
}
headRepo, err := resolved.HeadRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(headRepo), "MYSELF/REPO")
headRemote, err := resolved.RemoteForRepo(headRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if headRemote.Name != "fork" {
t.Errorf("got remote %q", headRemote.Name)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Input struct {
RepositoryID string
Title string
Body string
BaseRefName string
HeadRefName string
}
}
}{}
json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID0")
eq(t, reqBody.Variables.Input.Title, "cross repo")
eq(t, reqBody.Variables.Input.Body, "same branch")
eq(t, reqBody.Variables.Input.BaseRefName, "default")
eq(t, reqBody.Variables.Input.HeadRefName, "MYSELF:default")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
// goal: only care that gql is formatted properly
}

View file

@ -73,6 +73,7 @@ func RunCommand(cmd *cobra.Command, args string) (*cmdOut, error) {
func TestPRStatus(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prStatus.json")
defer jsonFile.Close()
@ -100,6 +101,7 @@ func TestPRStatus(t *testing.T) {
func TestPRStatus_reviewsAndChecks(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prStatusChecks.json")
defer jsonFile.Close()
@ -111,9 +113,9 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
}
expected := []string{
"- Checks passing - changes requested",
"- Checks pending - approved",
"- 1/3 checks failing - review required",
"- Checks passing - Changes requested",
"- Checks pending - Approved",
"- 1/3 checks failing - Review required",
}
for _, line := range expected {
@ -126,6 +128,7 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
func TestPRStatus_blankSlate(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": {} }
@ -157,6 +160,7 @@ Requesting a code review from you
func TestPRList(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prList.json")
defer jsonFile.Close()
@ -176,6 +180,7 @@ func TestPRList(t *testing.T) {
func TestPRList_filtering(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
@ -192,7 +197,7 @@ Pull requests for OWNER/REPO
No pull requests match your search
`)
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
State []string
@ -208,6 +213,7 @@ No pull requests match your search
func TestPRList_filteringAssignee(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
@ -217,7 +223,7 @@ func TestPRList_filteringAssignee(t *testing.T) {
t.Fatal(err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
reqBody := struct {
Variables struct {
Q string
@ -231,6 +237,7 @@ func TestPRList_filteringAssignee(t *testing.T) {
func TestPRList_filteringAssigneeLabels(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
@ -244,6 +251,7 @@ func TestPRList_filteringAssigneeLabels(t *testing.T) {
func TestPRView_preview(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prViewPreview.json")
defer jsonFile.Close()
@ -273,6 +281,7 @@ func TestPRView_preview(t *testing.T) {
func TestPRView_previewCurrentBranch(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prView.json")
defer jsonFile.Close()
@ -304,9 +313,44 @@ func TestPRView_previewCurrentBranch(t *testing.T) {
}
}
func TestPRView_previewCurrentBranchWithEmptyBody(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prView_EmptyBody.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
return &outputStub{}
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -p")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
eq(t, output.Stderr(), "")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Blueberries are a good fruit`),
regexp.MustCompile(`nobody wants to merge 8 commits into master from blueberries`),
regexp.MustCompile(`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`),
}
for _, r := range expectedLines {
if !r.MatchString(output.String()) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
return
}
}
}
func TestPRView_currentBranch(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prView.json")
defer jsonFile.Close()
@ -344,6 +388,7 @@ func TestPRView_currentBranch(t *testing.T) {
func TestPRView_noResultsForBranch(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
jsonFile, _ := os.Open("../test/fixtures/prView_NoActiveBranch.json")
defer jsonFile.Close()
@ -374,6 +419,7 @@ func TestPRView_noResultsForBranch(t *testing.T) {
func TestPRView_numberArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequest": {
@ -405,6 +451,7 @@ func TestPRView_numberArg(t *testing.T) {
func TestPRView_urlArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequest": {
@ -436,6 +483,7 @@ func TestPRView_urlArg(t *testing.T) {
func TestPRView_branchArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
@ -469,6 +517,7 @@ func TestPRView_branchArg(t *testing.T) {
func TestPRView_branchWithOwnerArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [

View file

@ -5,25 +5,37 @@ import (
"io"
"os"
"regexp"
"runtime/debug"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
// Version is dynamically set at build time in the Makefile
// Version is dynamically set by the toolchain or overriden by the Makefile.
var Version = "DEV"
// BuildDate is dynamically set at build time in the Makefile
var BuildDate = "YYYY-MM-DD"
// BuildDate is dynamically set at build time in the Makefile.
var BuildDate = "" // YYYY-MM-DD
var versionOutput = ""
func init() {
RootCmd.Version = fmt.Sprintf("%s (%s)", strings.TrimPrefix(Version, "v"), BuildDate)
if Version == "DEV" {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
Version = info.Main.Version
}
}
Version = strings.TrimPrefix(Version, "v")
if BuildDate == "" {
RootCmd.Version = Version
} else {
RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate)
}
versionOutput = fmt.Sprintf("gh version %s\n%s\n", RootCmd.Version, changelogURL(Version))
RootCmd.AddCommand(versionCmd)
RootCmd.SetVersionTemplate(versionOutput)
@ -151,3 +163,33 @@ func changelogURL(version string) string {
url := fmt.Sprintf("%s/releases/tag/v%s", path, strings.TrimPrefix(version, "v"))
return url
}
func determineBaseRepo(cmd *cobra.Command, ctx context.Context) (*ghrepo.Interface, error) {
apiClient, err := apiClientForContext(ctx)
if err != nil {
return nil, err
}
baseOverride, err := cmd.Flags().GetString("repo")
if err != nil {
return nil, err
}
remotes, err := ctx.Remotes()
if err != nil {
return nil, err
}
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, baseOverride)
if err != nil {
return nil, err
}
var baseRepo ghrepo.Interface
baseRepo, err = repoContext.BaseRepo()
if err != nil {
return nil, err
}
return &baseRepo, nil
}

View file

@ -12,6 +12,9 @@ func initBlankContext(repo, branch string) {
ctx := context.NewBlank()
ctx.SetBaseRepo(repo)
ctx.SetBranch(branch)
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
return ctx
}
}

View file

@ -18,10 +18,10 @@ const (
)
var (
// The GitHub app that is meant for development
oauthClientID = "4d747ba5675d5d66553f"
// The "GitHub CLI" OAuth app
oauthClientID = "178c6fc778ccc68e1d6a"
// This value is safe to be embedded in version control
oauthClientSecret = "d4fee7b3f9c2ef4284a5ca7be0ee200cf15b6f8d"
oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b"
)
// TODO: have a conversation about whether this belongs in the "context" package

View file

@ -1,8 +1,12 @@
package context
import (
"errors"
"fmt"
"path"
"sort"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/mitchellh/go-homedir"
@ -20,6 +24,100 @@ type Context interface {
SetBaseRepo(string)
}
// cap the number of git remotes looked up, since the user might have an
// unusally large number of git remotes
const maxRemotesForLookup = 5
func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (ResolvedRemotes, error) {
sort.Stable(remotes)
lenRemotesForLookup := len(remotes)
if lenRemotesForLookup > maxRemotesForLookup {
lenRemotesForLookup = maxRemotesForLookup
}
hasBaseOverride := base != ""
baseOverride := ghrepo.FromFullName(base)
foundBaseOverride := false
repos := []ghrepo.Interface{}
for _, r := range remotes[:lenRemotesForLookup] {
repos = append(repos, r)
if ghrepo.IsSame(r, baseOverride) {
foundBaseOverride = true
}
}
if hasBaseOverride && !foundBaseOverride {
// additionally, look up the explicitly specified base repo if it's not
// already covered by git remotes
repos = append(repos, baseOverride)
}
result := ResolvedRemotes{Remotes: remotes}
if hasBaseOverride {
result.BaseOverride = baseOverride
}
networkResult, err := api.RepoNetwork(client, repos)
if err != nil {
return result, err
}
result.Network = networkResult
return result, nil
}
type ResolvedRemotes struct {
BaseOverride ghrepo.Interface
Remotes Remotes
Network api.RepoNetworkResult
}
// BaseRepo is the first found repository in the "upstream", "github", "origin"
// git remote order, resolved to the parent repo if the git remote points to a fork
func (r ResolvedRemotes) BaseRepo() (*api.Repository, error) {
if r.BaseOverride != nil {
for _, repo := range r.Network.Repositories {
if repo != nil && ghrepo.IsSame(repo, r.BaseOverride) {
return repo, nil
}
}
return nil, fmt.Errorf("failed looking up information about the '%s' repository",
ghrepo.FullName(r.BaseOverride))
}
for _, repo := range r.Network.Repositories {
if repo == nil {
continue
}
if repo.IsFork() {
return repo.Parent, nil
}
return repo, nil
}
return nil, errors.New("not found")
}
// HeadRepo is the first found repository that has push access
func (r ResolvedRemotes) HeadRepo() (*api.Repository, error) {
for _, repo := range r.Network.Repositories {
if repo != nil && repo.ViewerCanPush() {
return repo, nil
}
}
return nil, errors.New("none of the repositories have push access")
}
// RemoteForRepo finds the git remote that points to a repository
func (r ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) {
for i, remote := range r.Remotes {
if ghrepo.IsSame(remote, repo) ||
// additionally, look up the resolved repository name in case this
// git remote points to this repository via a redirect
(r.Network.Repositories[i] != nil && ghrepo.IsSame(r.Network.Repositories[i], repo)) {
return remote, nil
}
}
return nil, errors.New("not found")
}
// New initializes a Context that reads from the filesystem
func New() Context {
return &fsContext{}

View file

@ -5,7 +5,9 @@ import (
"net/url"
"testing"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
)
func Test_Remotes_FindByName(t *testing.T) {
@ -57,3 +59,111 @@ func Test_translateRemotes(t *testing.T) {
t.Errorf("got %q", result[0].RepoName())
}
}
func Test_resolvedRemotes_triangularSetup(t *testing.T) {
resolved := ResolvedRemotes{
BaseOverride: nil,
Remotes: Remotes{
&Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
},
&Remote{
Remote: &git.Remote{Name: "fork"},
Owner: "MYSELF",
Repo: "REPO",
},
},
Network: api.RepoNetworkResult{
Repositories: []*api.Repository{
&api.Repository{
Name: "NEWNAME",
Owner: api.RepositoryOwner{Login: "NEWOWNER"},
ViewerPermission: "READ",
},
&api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "MYSELF"},
ViewerPermission: "ADMIN",
},
},
},
}
baseRepo, err := resolved.BaseRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(baseRepo), "NEWOWNER/NEWNAME")
baseRemote, err := resolved.RemoteForRepo(baseRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if baseRemote.Name != "origin" {
t.Errorf("got remote %q", baseRemote.Name)
}
headRepo, err := resolved.HeadRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(headRepo), "MYSELF/REPO")
headRemote, err := resolved.RemoteForRepo(headRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if headRemote.Name != "fork" {
t.Errorf("got remote %q", headRemote.Name)
}
}
func Test_resolvedRemotes_clonedFork(t *testing.T) {
resolved := ResolvedRemotes{
BaseOverride: nil,
Remotes: Remotes{
&Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
},
},
Network: api.RepoNetworkResult{
Repositories: []*api.Repository{
&api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "OWNER"},
ViewerPermission: "ADMIN",
Parent: &api.Repository{
Name: "REPO",
Owner: api.RepositoryOwner{Login: "PARENTOWNER"},
ViewerPermission: "READ",
},
},
},
},
}
baseRepo, err := resolved.BaseRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(baseRepo), "PARENTOWNER/REPO")
baseRemote, err := resolved.RemoteForRepo(baseRepo)
if baseRemote != nil || err == nil {
t.Error("did not expect any remote for base")
}
headRepo, err := resolved.HeadRepo()
if err != nil {
t.Fatalf("got %v", err)
}
eq(t, ghrepo.FullName(headRepo), "OWNER/REPO")
headRemote, err := resolved.RemoteForRepo(headRepo)
if err != nil {
t.Fatalf("got %v", err)
}
if headRemote.Name != "origin" {
t.Errorf("got remote %q", headRemote.Name)
}
}

View file

@ -91,7 +91,7 @@ func AddRemote(name, initURL, finalURL string) (*Remote, error) {
}
}
finalURLParsed, err := url.Parse(initURL)
finalURLParsed, err := url.Parse(finalURL)
if err != nil {
return nil, err
}

View file

@ -25,11 +25,11 @@ var (
func init() {
if runtime.GOOS == "windows" {
editor = "notepad"
}
if v := os.Getenv("VISUAL"); v != "" {
} else if g := os.Getenv("GIT_EDITOR"); g != "" {
editor = g
} else if v := os.Getenv("VISUAL"); v != "" {
editor = v
}
if e := os.Getenv("EDITOR"); e != "" {
} else if e := os.Getenv("EDITOR"); e != "" {
editor = e
}
}

View file

@ -1,5 +1,11 @@
# Installation from source
0. Verify that you have Go 1.13+ installed
```
$ go version
go version go1.13.7
```
1. Clone cli into `~/.githubcli`
```
$ git clone https://github.com/cli/cli.git ~/.githubcli

46
test/fixtures/prView_EmptyBody.json vendored Normal file
View file

@ -0,0 +1,46 @@
{
"data": {
"repository": {
"pullRequests": {
"nodes": [
{
"number": 12,
"title": "Blueberries are from a fork",
"body": "yeah",
"url": "https://github.com/OWNER/REPO/pull/12",
"headRefName": "blueberries",
"baseRefName": "master",
"headRepositoryOwner": {
"login": "hubot"
},
"commits": {
"totalCount": 12
},
"author": {
"login": "nobody"
},
"isCrossRepository": true
},
{
"number": 10,
"title": "Blueberries are a good fruit",
"body": "",
"url": "https://github.com/OWNER/REPO/pull/10",
"baseRefName": "master",
"headRefName": "blueberries",
"author": {
"login": "nobody"
},
"headRepositoryOwner": {
"login": "OWNER"
},
"commits": {
"totalCount": 8
},
"isCrossRepository": false
}
]
}
}
}
}

View file

@ -21,7 +21,7 @@ type StateEntry struct {
LatestRelease ReleaseInfo `yaml:"latest_release"`
}
// CheckForUpdate checks whether this software has had a newer relase on GitHub
// CheckForUpdate checks whether this software has had a newer release on GitHub
func CheckForUpdate(client *api.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) {
latestRelease, err := getLatestReleaseInfo(client, stateFilePath, repo, currentVersion)
if err != nil {

View file

@ -176,7 +176,10 @@ func (t *tsvTablePrinter) Render() error {
func truncate(maxLength int, title string) string {
if len(title) > maxLength {
return title[0:maxLength-3] + "..."
if maxLength > 3 {
return title[0:maxLength-3] + "..."
}
return title[0:maxLength]
}
return title
}

View file

@ -0,0 +1,31 @@
package utils
import (
"bytes"
"testing"
)
func Test_ttyTablePrinter_truncate(t *testing.T) {
buf := bytes.Buffer{}
tp := &ttyTablePrinter{
out: &buf,
maxWidth: 5,
}
tp.AddField("1", nil, nil)
tp.AddField("hello", nil, nil)
tp.EndRow()
tp.AddField("2", nil, nil)
tp.AddField("world", nil, nil)
tp.EndRow()
err := tp.Render()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "1 he\n2 wo\n"
if buf.String() != expected {
t.Errorf("expected: %q, got: %q", expected, buf.String())
}
}