Merge remote-tracking branch 'origin/master' into pr-create-push-default

This commit is contained in:
Mislav Marohnić 2020-03-30 13:32:37 +02:00
commit da2116f8ee
17 changed files with 739 additions and 315 deletions

View file

@ -22,7 +22,9 @@ func TestPullRequest_ChecksStatus(t *testing.T) {
{ "status": "COMPLETED",
"conclusion": "FAILURE" },
{ "status": "COMPLETED",
"conclusion": "ACTION_REQUIRED" }
"conclusion": "ACTION_REQUIRED" },
{ "status": "COMPLETED",
"conclusion": "STALE" }
]
}
}
@ -32,8 +34,8 @@ func TestPullRequest_ChecksStatus(t *testing.T) {
eq(t, err, nil)
checks := pr.ChecksStatus()
eq(t, checks.Total, 7)
eq(t, checks.Pending, 2)
eq(t, checks.Total, 8)
eq(t, checks.Pending, 3)
eq(t, checks.Failing, 3)
eq(t, checks.Passing, 2)
}

View file

@ -120,7 +120,7 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
summary.Passing++
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
summary.Failing++
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS":
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
summary.Pending++
default:
panic(fmt.Errorf("unsupported status: %q", state))

View file

@ -2,6 +2,7 @@ package api
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -10,16 +11,18 @@ import (
"time"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/utils"
)
// Repository contains information about a GitHub repo
type Repository struct {
ID string
Name string
URL string
CloneURL string
CreatedAt time.Time
Owner RepositoryOwner
ID string
Name string
Description string
URL string
CloneURL string
CreatedAt time.Time
Owner RepositoryOwner
IsPrivate bool
HasIssuesEnabled bool
@ -70,6 +73,7 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
repository(owner: $owner, name: $name) {
id
hasIssuesEnabled
description
}
}`
variables := map[string]interface{}{
@ -323,3 +327,43 @@ func RepoCreate(client *Client, input RepoCreateInput) (*Repository, error) {
return &response.CreateRepository.Repository, nil
}
func RepositoryReadme(client *Client, fullName string) (string, error) {
type readmeResponse struct {
Name string
Content string
}
var readme readmeResponse
err := client.REST("GET", fmt.Sprintf("repos/%s/readme", fullName), nil, &readme)
if err != nil && !strings.HasSuffix(err.Error(), "'Not Found'") {
return "", fmt.Errorf("could not get readme for repo: %w", err)
}
decoded, err := base64.StdEncoding.DecodeString(readme.Content)
if err != nil {
return "", fmt.Errorf("failed to decode readme: %w", err)
}
readmeContent := string(decoded)
if isMarkdownFile(readme.Name) {
readmeContent, err = utils.RenderMarkdown(readmeContent)
if err != nil {
return "", fmt.Errorf("failed to render readme as markdown: %w", err)
}
}
return readmeContent, nil
}
func isMarkdownFile(filename string) bool {
// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
// seem worth executing a regex for this given that assumption.
return strings.HasSuffix(filename, ".md") ||
strings.HasSuffix(filename, ".markdown") ||
strings.HasSuffix(filename, ".mdown") ||
strings.HasSuffix(filename, ".mkdown")
}

View file

@ -38,7 +38,7 @@ func init() {
issueListCmd.Flags().StringP("author", "A", "", "Filter by author")
issueCmd.AddCommand(issueViewCmd)
issueViewCmd.Flags().BoolP("preview", "p", false, "Display preview of issue content")
issueViewCmd.Flags().BoolP("web", "w", false, "Open issue in browser")
}
var issueCmd = &cobra.Command{
@ -73,7 +73,7 @@ var issueViewCmd = &cobra.Command{
}
return nil
},
Short: "View an issue in the browser",
Short: "View an issue",
RunE: issueView,
}
@ -213,17 +213,17 @@ func issueView(cmd *cobra.Command, args []string) error {
}
openURL := issue.URL
preview, err := cmd.Flags().GetBool("preview")
web, err := cmd.Flags().GetBool("web")
if err != nil {
return err
}
if preview {
out := colorableOut(cmd)
return printIssuePreview(out, issue)
} else {
if web {
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL)
return utils.OpenInBrowser(openURL)
} else {
out := colorableOut(cmd)
return printIssuePreview(out, issue)
}
}

View file

@ -216,79 +216,79 @@ func TestIssueList_disabledIssues(t *testing.T) {
}
}
func TestIssueView_web(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,
"url": "https://github.com/OWNER/REPO/issues/123"
} } } }
`))
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view -w 123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/OWNER/REPO/issues/123")
}
func TestIssueView_web_numberArgWithHash(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,
"url": "https://github.com/OWNER/REPO/issues/123"
} } } }
`))
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view -w \"#123\"")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/OWNER/REPO/issues/123")
}
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": {
"number": 123,
"url": "https://github.com/OWNER/REPO/issues/123"
} } } }
`))
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view 123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/OWNER/REPO/issues/123")
}
func TestIssueView_numberArgWithHash(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,
"url": "https://github.com/OWNER/REPO/issues/123"
} } } }
`))
var seenCmd *exec.Cmd
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view \"#123\"")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/OWNER/REPO/issues/123")
}
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": {
"number": 123,
@ -309,28 +309,21 @@ func TestIssueView_preview(t *testing.T) {
} } } }
`))
output, err := RunCommand(issueViewCmd, "issue view -p 123")
output, err := RunCommand(issueViewCmd, "issue view 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(`bold story`),
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
}
}
test.ExpectLines(t, output.String(),
"ix of coins",
`opened by marseilles. 9 comments. \(tarot\)`,
"bold story",
"View this issue on GitHub: https://github.com/OWNER/REPO/issues/123")
}
func TestIssueView_previewWithEmptyBody(t *testing.T) {
func TestIssueView_WithEmptyBody(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -355,27 +348,20 @@ func TestIssueView_previewWithEmptyBody(t *testing.T) {
} } } }
`))
output, err := RunCommand(issueViewCmd, "issue view -p 123")
output, err := RunCommand(issueViewCmd, "issue view 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
}
}
test.ExpectLines(t, output.String(),
"ix of coins",
`opened by marseilles. 9 comments. \(tarot\)`,
"View this issue on GitHub: https://github.com/OWNER/REPO/issues/123")
}
func TestIssueView_notFound(t *testing.T) {
func TestIssueView_web_notFound(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
@ -392,7 +378,7 @@ func TestIssueView_notFound(t *testing.T) {
})
defer restoreCmd()
_, err := RunCommand(issueViewCmd, "issue view 9999")
_, err := RunCommand(issueViewCmd, "issue view -w 9999")
if err == nil || err.Error() != "graphql error: 'Could not resolve to an Issue with the number of 9999.'" {
t.Errorf("error running command `issue view`: %v", err)
}
@ -420,7 +406,7 @@ func TestIssueView_disabledIssues(t *testing.T) {
}
}
func TestIssueView_urlArg(t *testing.T) {
func TestIssueView_web_urlArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -439,7 +425,7 @@ func TestIssueView_urlArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view https://github.com/OWNER/REPO/issues/123")
output, err := RunCommand(issueViewCmd, "issue view -w https://github.com/OWNER/REPO/issues/123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}

View file

@ -31,7 +31,7 @@ func init() {
prListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label")
prListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
prViewCmd.Flags().BoolP("preview", "p", false, "Display preview of pull request content")
prViewCmd.Flags().BoolP("web", "w", false, "Open pull request in browser")
}
var prCmd = &cobra.Command{
@ -57,10 +57,10 @@ var prStatusCmd = &cobra.Command{
var prViewCmd = &cobra.Command{
Use: "view [{<number> | <url> | <branch>}]",
Short: "View a pull request in the browser",
Long: `View a pull request specified by the argument in the browser.
Long: `View a pull request specified by the argument.
Without an argument, the pull request that belongs to the current
branch is opened.`,
branch is displayed.`,
RunE: prView,
}
@ -272,7 +272,7 @@ func prView(cmd *cobra.Command, args []string) error {
}
}
preview, err := cmd.Flags().GetBool("preview")
web, err := cmd.Flags().GetBool("web")
if err != nil {
return err
}
@ -293,7 +293,7 @@ func prView(cmd *cobra.Command, args []string) error {
if prNumber > 0 {
openURL = fmt.Sprintf("https://github.com/%s/pull/%d", ghrepo.FullName(baseRepo), prNumber)
if preview {
if !web {
pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNumber)
if err != nil {
return err
@ -309,12 +309,12 @@ func prView(cmd *cobra.Command, args []string) error {
}
}
if preview {
out := colorableOut(cmd)
return printPrPreview(out, pr)
} else {
if web {
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL)
return utils.OpenInBrowser(openURL)
} else {
out := colorableOut(cmd)
return printPrPreview(out, pr)
}
}
@ -418,7 +418,9 @@ func printPrs(w io.Writer, totalCount int, prs ...api.PullRequest) {
reviews := pr.ReviewStatus()
if pr.State == "OPEN" {
if checks.Total > 0 || reviews.ChangesRequested || reviews.Approved {
reviewStatus := reviews.ChangesRequested || reviews.Approved || reviews.ReviewRequired
if checks.Total > 0 || reviewStatus {
// show checks & reviews on their own line
fmt.Fprintf(w, "\n ")
}
@ -426,24 +428,29 @@ func printPrs(w io.Writer, totalCount int, prs ...api.PullRequest) {
var summary string
if checks.Failing > 0 {
if checks.Failing == checks.Total {
summary = utils.Red("All checks failing")
summary = utils.Red("× All checks failing")
} else {
summary = utils.Red(fmt.Sprintf("%d/%d checks failing", checks.Failing, checks.Total))
summary = utils.Red(fmt.Sprintf("× %d/%d checks failing", checks.Failing, checks.Total))
}
} else if checks.Pending > 0 {
summary = utils.Yellow("Checks pending")
summary = utils.Yellow("- Checks pending")
} else if checks.Passing == checks.Total {
summary = utils.Green("Checks passing")
summary = utils.Green("Checks passing")
}
fmt.Fprintf(w, " - %s", summary)
fmt.Fprint(w, summary)
}
if checks.Total > 0 && reviewStatus {
// add padding between checks & reviews
fmt.Fprint(w, " ")
}
if reviews.ChangesRequested {
fmt.Fprintf(w, " - %s", utils.Red("Changes requested"))
fmt.Fprint(w, utils.Red("+ Changes requested"))
} else if reviews.ReviewRequired {
fmt.Fprintf(w, " - %s", utils.Yellow("Review required"))
fmt.Fprint(w, utils.Yellow("- Review required"))
} else if reviews.Approved {
fmt.Fprintf(w, " - %s", utils.Green("Approved"))
fmt.Fprint(w, utils.Green("Approved"))
}
} else {
s := strings.Title(strings.ToLower(pr.State))

View file

@ -66,7 +66,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
cmdQueue = append(cmdQueue, []string{"git", "fetch", headRemote.Name, refSpec})
// local branch already exists
if git.VerifyRef("refs/heads/" + newBranchName) {
if _, err := git.ShowRefs("refs/heads/" + newBranchName); err == nil {
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
} else {

View file

@ -46,7 +46,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
@ -98,7 +98,7 @@ func TestPRCheckout_urlArg(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
@ -147,7 +147,7 @@ func TestPRCheckout_urlArg_differentBase(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
@ -210,7 +210,7 @@ func TestPRCheckout_branchArg(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)
@ -260,7 +260,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &test.OutputStub{}
default:
ranCommands = append(ranCommands, cmd.Args)
@ -313,7 +313,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
ranCommands := [][]string{}
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
switch strings.Join(cmd.Args, " ") {
case "git show-ref --verify --quiet refs/heads/feature":
case "git show-ref --verify -- refs/heads/feature":
return &errorStub{"exit status: 1"}
default:
ranCommands = append(ranCommands, cmd.Args)

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/cli/cli/api"
@ -75,7 +76,27 @@ func prCreate(cmd *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("could not determine the current branch: %w", err)
}
headRepo, headRepoErr := repoContext.HeadRepo()
var headRepo ghrepo.Interface
var headRemote *context.Remote
// determine whether the head branch is already pushed to a remote
headBranchPushedTo := determineTrackingBranch(remotes, headBranch)
if headBranchPushedTo != nil {
for _, r := range remotes {
if r.Name != headBranchPushedTo.RemoteName {
continue
}
headRepo = r
headRemote = r
break
}
}
// otherwise, determine the head repository with info obtained from the API
if headRepo == nil {
headRepo, _ = repoContext.HeadRepo()
}
baseBranch, err := cmd.Flags().GetString("base")
if err != nil {
@ -193,7 +214,9 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
didForkRepo := false
if headRepoErr != nil {
// if a head repository could not be determined so far, automatically create
// one by forking the base repository
if headRepo == nil {
if baseRepo.IsPrivate {
return fmt.Errorf("cannot fork private repository '%s'", ghrepo.FullName(baseRepo))
}
@ -209,7 +232,6 @@ func prCreate(cmd *cobra.Command, _ []string) error {
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
}
headRemote, err := repoContext.RemoteForRepo(headRepo)
// There are two cases when an existing remote for the head repo will be
// missing:
// 1. the head repo was just created by auto-forking;
@ -232,22 +254,31 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
}
pushTries := 0
maxPushTries := 3
for {
// TODO: respect existing upstream configuration of the current branch
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
if didForkRepo && pushTries < maxPushTries {
pushTries++
// first wait 2 seconds after forking, then 4s, then 6s
waitSeconds := 2 * pushTries
fmt.Fprintf(cmd.ErrOrStderr(), "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
time.Sleep(time.Duration(waitSeconds) * time.Second)
continue
// automatically push the branch if it hasn't been pushed anywhere yet
if headBranchPushedTo == nil {
if headRemote == nil {
headRemote, err = repoContext.RemoteForRepo(headRepo)
if err != nil {
return fmt.Errorf("git remote not found for head repository: %w", err)
}
return err
}
break
pushTries := 0
maxPushTries := 3
for {
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
if didForkRepo && pushTries < maxPushTries {
pushTries++
// first wait 2 seconds after forking, then 4s, then 6s
waitSeconds := 2 * pushTries
fmt.Fprintf(cmd.ErrOrStderr(), "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
time.Sleep(time.Duration(waitSeconds) * time.Second)
continue
}
return err
}
break
}
}
if action == SubmitAction {
@ -277,6 +308,47 @@ func prCreate(cmd *cobra.Command, _ []string) error {
return nil
}
func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.TrackingRef {
refsForLookup := []string{"HEAD"}
var trackingRefs []git.TrackingRef
headBranchConfig := git.ReadBranchConfig(headBranch)
if headBranchConfig.RemoteName != "" {
tr := git.TrackingRef{
RemoteName: headBranchConfig.RemoteName,
BranchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"),
}
trackingRefs = append(trackingRefs, tr)
refsForLookup = append(refsForLookup, tr.String())
}
for _, remote := range remotes {
tr := git.TrackingRef{
RemoteName: remote.Name,
BranchName: headBranch,
}
trackingRefs = append(trackingRefs, tr)
refsForLookup = append(refsForLookup, tr.String())
}
resolvedRefs, _ := git.ShowRefs(refsForLookup...)
if len(resolvedRefs) > 1 {
for _, r := range resolvedRefs[1:] {
if r.Hash != resolvedRefs[0].Hash {
continue
}
for _, tr := range trackingRefs {
if tr.String() != r.Name {
continue
}
return &tr
}
}
}
return nil
}
func generateCompareURL(r ghrepo.Interface, base, head, title, body string) string {
u := fmt.Sprintf(
"https://github.com/%s/compare/%s...%s?expand=1",

View file

@ -8,6 +8,7 @@ import (
"testing"
"github.com/cli/cli/context"
"github.com/cli/cli/git"
)
func TestPRCreate(t *testing.T) {
@ -31,6 +32,8 @@ func TestPRCreate(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
@ -80,6 +83,8 @@ func TestPRCreate_alreadyExists(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
@ -112,6 +117,8 @@ func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git rev-parse
@ -134,6 +141,8 @@ func TestPRCreate_web(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
@ -145,9 +154,9 @@ func TestPRCreate_web(t *testing.T) {
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n")
eq(t, len(cs.Calls), 4)
eq(t, strings.Join(cs.Calls[2].Args, " "), "git push --set-upstream origin HEAD:feature")
browserCall := cs.Calls[3].Args
eq(t, len(cs.Calls), 6)
eq(t, strings.Join(cs.Calls[4].Args, " "), "git push --set-upstream origin HEAD:feature")
browserCall := cs.Calls[5].Args
eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1")
}
@ -173,6 +182,8 @@ func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub(" M git/git.go") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
@ -243,6 +254,8 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
@ -296,6 +309,8 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git rev-parse
@ -370,6 +385,8 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,the sky above the port") // git log
cs.Stub("was the color of a television, turned to a dead channel") // git show
@ -445,6 +462,8 @@ func TestPRCreate_survey_autofill(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("1234567890,the sky above the port") // git log
cs.Stub("was the color of a television, turned to a dead channel") // git show
@ -488,6 +507,8 @@ func TestPRCreate_defaults_error_autofill(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("") // git log
@ -504,6 +525,8 @@ func TestPRCreate_defaults_error_web(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("") // git log
@ -529,6 +552,8 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
cs.Stub("") // git status
cs.Stub("") // git log
cs.Stub("") // git rev-parse
@ -561,3 +586,103 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) {
stderr := string(output.Stderr())
eq(t, strings.Contains(stderr, "warning: could not compute title or body defaults: could not find any commits"), true)
}
func Test_determineTrackingBranch_empty(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
remotes := context.Remotes{}
cs.Stub("") // git config --get-regexp (ReadBranchConfig)
cs.Stub("deadbeef HEAD") // git show-ref --verify (ShowRefs)
ref := determineTrackingBranch(remotes, "feature")
if ref != nil {
t.Errorf("expected nil result, got %v", ref)
}
}
func Test_determineTrackingBranch_noMatch(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
},
&context.Remote{
Remote: &git.Remote{Name: "upstream"},
Owner: "octocat",
Repo: "Spoon-Knife",
},
}
cs.Stub("") // git config --get-regexp (ReadBranchConfig)
cs.Stub(`deadbeef HEAD
deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs)
ref := determineTrackingBranch(remotes, "feature")
if ref != nil {
t.Errorf("expected nil result, got %v", ref)
}
}
func Test_determineTrackingBranch_hasMatch(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
},
&context.Remote{
Remote: &git.Remote{Name: "upstream"},
Owner: "octocat",
Repo: "Spoon-Knife",
},
}
cs.Stub("") // git config --get-regexp (ReadBranchConfig)
cs.Stub(`deadbeef HEAD
deadb00f refs/remotes/origin/feature
deadbeef refs/remotes/upstream/feature`) // git show-ref --verify (ShowRefs)
ref := determineTrackingBranch(remotes, "feature")
if ref == nil {
t.Fatal("expected result, got nil")
}
eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/feature", "refs/remotes/upstream/feature"})
eq(t, ref.RemoteName, "upstream")
eq(t, ref.BranchName, "feature")
}
func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) {
cs, cmdTeardown := initCmdStubber()
defer cmdTeardown()
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
},
}
cs.Stub(`branch.feature.remote origin
branch.feature.merge refs/heads/great-feat`) // git config --get-regexp (ReadBranchConfig)
cs.Stub(`deadbeef HEAD
deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs)
ref := determineTrackingBranch(remotes, "feature")
if ref != nil {
t.Errorf("expected nil result, got %v", ref)
}
eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/great-feat", "refs/remotes/origin/feature"})
}

View file

@ -144,9 +144,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 {
@ -409,25 +409,18 @@ func TestPRView_preview(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prViewCmd, "pr view -p 12")
output, err := RunCommand(prViewCmd, "pr view 12")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
eq(t, output.Stderr(), "")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Blueberries are from a fork`),
regexp.MustCompile(`nobody wants to merge 12 commits into master from blueberries`),
regexp.MustCompile(`blueberries taste good`),
regexp.MustCompile(`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`),
}
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
}
}
test.ExpectLines(t, output.String(),
"Blueberries are from a fork",
"nobody wants to merge 12 commits into master from blueberries",
"blueberries taste good",
"View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12")
}
func TestPRView_previewCurrentBranch(t *testing.T) {
@ -444,25 +437,18 @@ func TestPRView_previewCurrentBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -p")
output, err := RunCommand(prViewCmd, "pr view")
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(`blueberries taste good`),
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
}
}
test.ExpectLines(t, output.String(),
"Blueberries are a good fruit",
"nobody wants to merge 8 commits into master from blueberries",
"blueberries taste good",
"View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10")
}
func TestPRView_previewCurrentBranchWithEmptyBody(t *testing.T) {
@ -479,27 +465,20 @@ func TestPRView_previewCurrentBranchWithEmptyBody(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -p")
output, err := RunCommand(prViewCmd, "pr view")
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
}
}
test.ExpectLines(t, output.String(),
"Blueberries are a good fruit",
"nobody wants to merge 8 commits into master from blueberries",
"View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10")
}
func TestPRView_currentBranch(t *testing.T) {
func TestPRView_web_currentBranch(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -520,7 +499,7 @@ func TestPRView_currentBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view")
output, err := RunCommand(prViewCmd, "pr view -w")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -537,7 +516,7 @@ func TestPRView_currentBranch(t *testing.T) {
}
}
func TestPRView_noResultsForBranch(t *testing.T) {
func TestPRView_web_noResultsForBranch(t *testing.T) {
initBlankContext("OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -558,7 +537,7 @@ func TestPRView_noResultsForBranch(t *testing.T) {
})
defer restoreCmd()
_, err := RunCommand(prViewCmd, "pr view")
_, err := RunCommand(prViewCmd, "pr view -w")
if err == nil || err.Error() != `no open pull requests found for branch "blueberries"` {
t.Errorf("error running command `pr view`: %v", err)
}
@ -568,7 +547,7 @@ func TestPRView_noResultsForBranch(t *testing.T) {
}
}
func TestPRView_numberArg(t *testing.T) {
func TestPRView_web_numberArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -586,7 +565,7 @@ func TestPRView_numberArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view 23")
output, err := RunCommand(prViewCmd, "pr view -w 23")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -600,7 +579,7 @@ func TestPRView_numberArg(t *testing.T) {
eq(t, url, "https://github.com/OWNER/REPO/pull/23")
}
func TestPRView_numberArgWithHash(t *testing.T) {
func TestPRView_web_numberArgWithHash(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -618,7 +597,7 @@ func TestPRView_numberArgWithHash(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view \"#23\"")
output, err := RunCommand(prViewCmd, "pr view -w \"#23\"")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -632,7 +611,7 @@ func TestPRView_numberArgWithHash(t *testing.T) {
eq(t, url, "https://github.com/OWNER/REPO/pull/23")
}
func TestPRView_urlArg(t *testing.T) {
func TestPRView_web_urlArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
@ -649,7 +628,7 @@ func TestPRView_urlArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view https://github.com/OWNER/REPO/pull/23/files")
output, err := RunCommand(prViewCmd, "pr view -w https://github.com/OWNER/REPO/pull/23/files")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -663,7 +642,7 @@ func TestPRView_urlArg(t *testing.T) {
eq(t, url, "https://github.com/OWNER/REPO/pull/23")
}
func TestPRView_branchArg(t *testing.T) {
func TestPRView_web_branchArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -683,7 +662,7 @@ func TestPRView_branchArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view blueberries")
output, err := RunCommand(prViewCmd, "pr view -w blueberries")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -697,7 +676,7 @@ func TestPRView_branchArg(t *testing.T) {
eq(t, url, "https://github.com/OWNER/REPO/pull/23")
}
func TestPRView_branchWithOwnerArg(t *testing.T) {
func TestPRView_web_branchWithOwnerArg(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -718,7 +697,7 @@ func TestPRView_branchWithOwnerArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view hubot:blueberries")
output, err := RunCommand(prViewCmd, "pr view -w hubot:blueberries")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}

View file

@ -6,6 +6,7 @@ import (
"os"
"path"
"strings"
"text/template"
"time"
"github.com/AlecAivazis/survey/v2"
@ -35,6 +36,7 @@ func init() {
repoForkCmd.Flags().Lookup("remote").NoOptDefVal = "true"
repoCmd.AddCommand(repoViewCmd)
repoViewCmd.Flags().BoolP("web", "w", false, "Open repository in browser")
}
var repoCmd = &cobra.Command{
@ -78,9 +80,9 @@ With no argument, creates a fork of the current repository. Otherwise, forks the
var repoViewCmd = &cobra.Command{
Use: "view [<repository>]",
Short: "View a repository in the browser",
Long: `View a GitHub repository in the browser.
Long: `View a GitHub repository.
With no argument, the repository for the current directory is opened.`,
With no argument, the repository for the current directory is displayed.`,
RunE: repoView,
}
@ -301,14 +303,6 @@ func repoFork(cmd *cobra.Command, args []string) error {
s.FinalMSG = utils.Gray(fmt.Sprintf("- %s\n", loading))
s.Start()
authLogin, err := ctx.AuthLogin()
if err != nil {
s.Stop()
return fmt.Errorf("could not determine current username: %w", err)
}
possibleFork := ghrepo.New(authLogin, toFork.RepoName())
forkedRepo, err := api.ForkRepo(apiClient, toFork)
if err != nil {
s.Stop()
@ -321,11 +315,11 @@ func repoFork(cmd *cobra.Command, args []string) error {
// returns the fork repo data even if it already exists -- with no change in status code or
// anything. We thus check the created time to see if the repo is brand new or not; if it's not,
// we assume the fork already existed and report an error.
created_ago := Since(forkedRepo.CreatedAt)
if created_ago > time.Minute {
createdAgo := Since(forkedRepo.CreatedAt)
if createdAgo > time.Minute {
fmt.Fprintf(out, "%s %s %s\n",
utils.Yellow("!"),
utils.Bold(ghrepo.FullName(possibleFork)),
utils.Bold(ghrepo.FullName(forkedRepo)),
"already exists")
} else {
fmt.Fprintf(out, "%s Created fork %s\n", greenCheck, utils.Bold(ghrepo.FullName(forkedRepo)))
@ -336,6 +330,15 @@ func repoFork(cmd *cobra.Command, args []string) error {
}
if inParent {
remotes, err := ctx.Remotes()
if err != nil {
return err
}
if remote, err := remotes.FindByRepo(forkedRepo.RepoOwner(), forkedRepo.RepoName()); err == nil {
fmt.Fprintf(out, "%s Using existing remote %s\n", greenCheck, utils.Bold(remote.Name))
return nil
}
remoteDesired := remotePref == "true"
if remotePref == "prompt" {
err = Confirm("Would you like to add a remote for the fork?", &remoteDesired)
@ -386,6 +389,7 @@ var Confirm = func(prompt string, result *bool) error {
func repoView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
var toView ghrepo.Interface
if len(args) == 0 {
var err error
@ -414,12 +418,67 @@ func repoView(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
_, err = api.GitHubRepo(apiClient, toView)
repo, err := api.GitHubRepo(apiClient, toView)
if err != nil {
return err
}
openURL := fmt.Sprintf("https://github.com/%s", ghrepo.FullName(toView))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
return utils.OpenInBrowser(openURL)
web, err := cmd.Flags().GetBool("web")
if err != nil {
return err
}
fullName := ghrepo.FullName(toView)
openURL := fmt.Sprintf("https://github.com/%s", fullName)
if web {
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
return utils.OpenInBrowser(openURL)
}
repoTmpl := `
{{.FullName}}
{{.Description}}
{{.Readme}}
{{.View}}
`
tmpl, err := template.New("repo").Parse(repoTmpl)
if err != nil {
return err
}
readmeContent, _ := api.RepositoryReadme(apiClient, fullName)
if readmeContent == "" {
readmeContent = utils.Gray("No README provided")
}
description := repo.Description
if description == "" {
description = utils.Gray("No description provided")
}
repoData := struct {
FullName string
Description string
Readme string
View string
}{
FullName: utils.Bold(fullName),
Description: description,
Readme: readmeContent,
View: utils.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
}
out := colorableOut(cmd)
err = tmpl.Execute(out, repoData)
if err != nil {
return err
}
return nil
}

View file

@ -18,9 +18,8 @@ import (
func TestRepoFork_already_forked(t *testing.T) {
initContext = func() context.Context {
ctx := context.NewBlank()
ctx.SetBaseRepo("REPO")
ctx.SetBaseRepo("OWNER/REPO")
ctx.SetBranch("master")
ctx.SetAuthLogin("someone")
ctx.SetRemotes(map[string]string{
"origin": "OWNER/REPO",
})
@ -41,6 +40,31 @@ func TestRepoFork_already_forked(t *testing.T) {
}
}
func TestRepoFork_reuseRemote(t *testing.T) {
initContext = func() context.Context {
ctx := context.NewBlank()
ctx.SetBaseRepo("OWNER/REPO")
ctx.SetBranch("master")
ctx.SetRemotes(map[string]string{
"upstream": "OWNER/REPO",
"origin": "someone/REPO",
})
return ctx
}
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
defer http.StubWithFixture(200, "forkResult.json")()
output, err := RunCommand(repoForkCmd, "repo fork")
if err != nil {
t.Errorf("got unexpected error: %v", err)
}
if !strings.Contains(output.String(), "Using existing remote origin") {
t.Errorf("output did not match: %q", output)
return
}
}
func stubSince(d time.Duration) func() {
originalSince := Since
Since = func(t time.Time) time.Duration {
@ -137,16 +161,9 @@ func TestRepoFork_in_parent_yes(t *testing.T) {
eq(t, output.Stderr(), "")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Created fork someone/REPO`),
regexp.MustCompile(`Remote added at fork`),
}
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
}
}
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
"Remote added at fork")
}
func TestRepoFork_outside_yes(t *testing.T) {
@ -169,16 +186,9 @@ func TestRepoFork_outside_yes(t *testing.T) {
eq(t, strings.Join(seenCmd.Args, " "), "git clone https://github.com/someone/repo.git")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Created fork someone/REPO`),
regexp.MustCompile(`Cloned fork`),
}
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
}
}
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
"Cloned fork")
}
func TestRepoFork_outside_survey_yes(t *testing.T) {
@ -208,16 +218,9 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
eq(t, strings.Join(seenCmd.Args, " "), "git clone https://github.com/someone/repo.git")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Created fork someone/REPO`),
regexp.MustCompile(`Cloned fork`),
}
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
}
}
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
"Cloned fork")
}
func TestRepoFork_outside_survey_no(t *testing.T) {
@ -290,16 +293,9 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
eq(t, output.Stderr(), "")
expectedLines := []*regexp.Regexp{
regexp.MustCompile(`Created fork someone/REPO`),
regexp.MustCompile(`Remote added at fork`),
}
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
}
}
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
"Remote added at fork")
}
func TestRepoFork_in_parent_survey_no(t *testing.T) {
@ -580,8 +576,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
}
}
func TestRepoView(t *testing.T) {
func TestRepoView_web(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
@ -596,7 +591,7 @@ func TestRepoView(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view")
output, err := RunCommand(repoViewCmd, "repo view -w")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -611,7 +606,7 @@ func TestRepoView(t *testing.T) {
eq(t, url, "https://github.com/OWNER/REPO")
}
func TestRepoView_ownerRepo(t *testing.T) {
func TestRepoView_web_ownerRepo(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
@ -629,7 +624,7 @@ func TestRepoView_ownerRepo(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view cli/cli")
output, err := RunCommand(repoViewCmd, "repo view -w cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -644,7 +639,7 @@ func TestRepoView_ownerRepo(t *testing.T) {
eq(t, url, "https://github.com/cli/cli")
}
func TestRepoView_fullURL(t *testing.T) {
func TestRepoView_web_fullURL(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
@ -661,7 +656,7 @@ func TestRepoView_fullURL(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view https://github.com/cli/cli")
output, err := RunCommand(repoViewCmd, "repo view -w https://github.com/cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -675,3 +670,77 @@ func TestRepoView_fullURL(t *testing.T) {
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/cli/cli")
}
func TestRepoView(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": {
"repository": {
"description": "social distancing"
}}}
`))
http.StubResponse(200, bytes.NewBufferString(`
{ "name": "readme.md",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}
`))
output, err := RunCommand(repoViewCmd, "repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"social distancing",
"truly cool readme",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}
func TestRepoView_nonmarkdown_readme(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": {
"repository": {
"description": "social distancing"
}}}
`))
http.StubResponse(200, bytes.NewBufferString(`
{ "name": "readme.org",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}
`))
output, err := RunCommand(repoViewCmd, "repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"social distancing",
"# truly cool readme",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}
func TestRepoView_blanks(t *testing.T) {
initBlankContext("OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString("{}"))
http.StubResponse(200, bytes.NewBufferString("{}"))
output, err := RunCommand(repoViewCmd, "repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"No description provided",
"No README provided",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}

View file

@ -13,10 +13,41 @@ import (
"github.com/cli/cli/utils"
)
func VerifyRef(ref string) bool {
showRef := exec.Command("git", "show-ref", "--verify", "--quiet", ref)
err := utils.PrepareCmd(showRef).Run()
return err == nil
// Ref represents a git commit reference
type Ref struct {
Hash string
Name string
}
// TrackingRef represents a ref for a remote tracking branch
type TrackingRef struct {
RemoteName string
BranchName string
}
func (r TrackingRef) String() string {
return "refs/remotes/" + r.RemoteName + "/" + r.BranchName
}
// ShowRefs resolves fully-qualified refs to commit hashes
func ShowRefs(ref ...string) ([]Ref, error) {
args := append([]string{"show-ref", "--verify", "--"}, ref...)
showRef := exec.Command("git", args...)
output, err := utils.PrepareCmd(showRef).Output()
var refs []Ref
for _, line := range outputLines(output) {
parts := strings.SplitN(line, " ", 2)
if len(parts) < 2 {
continue
}
refs = append(refs, Ref{
Hash: parts[0],
Name: parts[1],
})
}
return refs, err
}
// CurrentBranch reads the checked-out branch for the git repository
@ -152,7 +183,7 @@ func ReadBranchConfig(branch string) (cfg BranchConfig) {
continue
}
cfg.RemoteURL = u
} else {
} else if !isFilesystemPath(parts[1]) {
cfg.RemoteName = parts[1]
}
case "merge":
@ -162,6 +193,10 @@ func ReadBranchConfig(branch string) (cfg BranchConfig) {
return
}
func isFilesystemPath(p string) bool {
return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/")
}
// ToplevelDir returns the top-level directory path of the current repository
func ToplevelDir() (string, error) {
showCmd := exec.Command("git", "rev-parse", "--show-toplevel")

View file

@ -8,21 +8,26 @@ import (
const defaultHostname = "github.com"
// Interface describes an object that represents a GitHub repository
type Interface interface {
RepoName() string
RepoOwner() string
}
// New instantiates a GitHub repository from owner and name arguments
func New(owner, repo string) Interface {
return &ghRepo{
owner: owner,
name: repo,
}
}
// FullName serializes a GitHub repository into an "OWNER/REPO" string
func FullName(r Interface) string {
return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName())
}
// FromFullName extracts the GitHub repository inforation from an "OWNER/REPO" string
func FromFullName(nwo string) Interface {
var r ghRepo
parts := strings.SplitN(nwo, "/", 2)
@ -32,8 +37,9 @@ func FromFullName(nwo string) Interface {
return &r
}
// FromURL extracts the GitHub repository information from a URL
func FromURL(u *url.URL) (Interface, error) {
if !strings.EqualFold(u.Hostname(), defaultHostname) {
if !strings.EqualFold(u.Hostname(), defaultHostname) && !strings.EqualFold(u.Hostname(), "www."+defaultHostname) {
return nil, fmt.Errorf("unsupported hostname: %s", u.Hostname())
}
parts := strings.SplitN(strings.TrimPrefix(u.Path, "/"), "/", 3)
@ -43,6 +49,7 @@ func FromURL(u *url.URL) (Interface, error) {
return New(parts[0], strings.TrimSuffix(parts[1], ".git")), nil
}
// IsSame compares two GitHub repositories
func IsSame(a, b Interface) bool {
return strings.EqualFold(a.RepoOwner(), b.RepoOwner()) &&
strings.EqualFold(a.RepoName(), b.RepoName())

View file

@ -1,40 +1,66 @@
package ghrepo
import (
"errors"
"fmt"
"net/url"
"testing"
)
func Test_repoFromURL(t *testing.T) {
u, _ := url.Parse("http://github.com/monalisa/octo-cat.git")
repo, err := FromURL(u)
if err != nil {
t.Fatalf("got error %q", err)
tests := []struct {
name string
input string
result string
err error
}{
{
name: "github.com URL",
input: "https://github.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
err: nil,
},
{
name: "www.github.com URL",
input: "http://www.GITHUB.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
err: nil,
},
{
name: "unsupported hostname",
input: "https://example.com/one/two",
result: "",
err: errors.New("unsupported hostname: example.com"),
},
{
name: "filesystem path",
input: "/path/to/file",
result: "",
err: errors.New("unsupported hostname: "),
},
}
if repo.RepoOwner() != "monalisa" {
t.Errorf("got owner %q", repo.RepoOwner())
}
if repo.RepoName() != "octo-cat" {
t.Errorf("got name %q", repo.RepoName())
}
}
func Test_repoFromURL_invalid(t *testing.T) {
cases := [][]string{
[]string{
"https://example.com/one/two",
"unsupported hostname: example.com",
},
[]string{
"/path/to/disk",
"unsupported hostname: ",
},
}
for _, c := range cases {
u, _ := url.Parse(c[0])
_, err := FromURL(u)
if err == nil || err.Error() != c[1] {
t.Errorf("got %q", err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, err := url.Parse(tt.input)
if err != nil {
t.Fatalf("got error %q", err)
}
repo, err := FromURL(u)
if err != nil {
if tt.err == nil {
t.Fatalf("got error %q", err)
} else if tt.err.Error() == err.Error() {
return
}
t.Fatalf("got error %q", err)
}
got := fmt.Sprintf("%s/%s", repo.RepoOwner(), repo.RepoName())
if tt.result != got {
t.Errorf("expected %q, got %q", tt.result, got)
}
})
}
}

View file

@ -6,6 +6,8 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"testing"
)
// OutputStub implements a simple utils.Runnable
@ -74,3 +76,14 @@ func UseTempGitRepo() *TempGitRepo {
return &TempGitRepo{Remote: remotePath, TearDown: tearDown}
}
func ExpectLines(t *testing.T, output string, lines ...string) {
var r *regexp.Regexp
for _, l := range lines {
r = regexp.MustCompile(l)
if !r.MatchString(output) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
return
}
}
}