Merge remote-tracking branch 'origin/master' into table-output
This commit is contained in:
commit
02b1f60a24
15 changed files with 1031 additions and 241 deletions
17
README.md
17
README.md
|
|
@ -4,8 +4,25 @@ The #ce-cli team is working on a publicly available CLI tool to reduce the frict
|
|||
|
||||
This tool is an endeavor separate from [github/hub](https://github.com/github/hub), which acts as a proxy to `git`, since our aim is to reimagine from scratch the kind of command line interface to GitHub that would serve our users' interests best.
|
||||
|
||||
# Installation
|
||||
|
||||
_warning, gh is in a very alpha phase_
|
||||
|
||||
`brew install github/gh/gh`
|
||||
|
||||
That's it. You are now ready to use `gh` on the command line. 🥳
|
||||
|
||||
# Process
|
||||
|
||||
- [Demo planning doc](https://docs.google.com/document/d/18ym-_xjFTSXe0-xzgaBn13Su7MEhWfLE5qSNPJV4M0A/edit)
|
||||
- [Weekly tracking issue](https://github.com/github/gh-cli/labels/tracking%20issue)
|
||||
- [Weekly sync notes](https://docs.google.com/document/d/1eUo9nIzXbC1DG26Y3dk9hOceLua2yFlwlvFPZ82MwHg/edit)
|
||||
|
||||
# How to create a release
|
||||
|
||||
This can all be done from your local terminal.
|
||||
|
||||
1. `git tag 'vVERSION_NUMBER' # example git tag 'v0.0.1'`
|
||||
2. `git push origin vVERSION_NUMBER`
|
||||
3. Wait a few minutes for the build to run and CI to pass. Look at the [actions tab](https://github.com/github/gh-cli/actions) to check the progress.
|
||||
4. Go to https://github.com/github/homebrew-gh/releases and look at the release
|
||||
|
|
|
|||
39
api/pull_request_test.go
Normal file
39
api/pull_request_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPullRequest_ChecksStatus(t *testing.T) {
|
||||
pr := PullRequest{}
|
||||
payload := `
|
||||
{ "commits": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{ "state": "SUCCESS" },
|
||||
{ "state": "PENDING" },
|
||||
{ "state": "FAILURE" },
|
||||
{ "status": "IN_PROGRESS",
|
||||
"conclusion": null },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "SUCCESS" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "FAILURE" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "ACTION_REQUIRED" }
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`
|
||||
err := json.Unmarshal([]byte(payload), &pr)
|
||||
eq(t, err, nil)
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
eq(t, checks.Total, 7)
|
||||
eq(t, checks.Pending, 2)
|
||||
eq(t, checks.Failing, 3)
|
||||
eq(t, checks.Passing, 2)
|
||||
}
|
||||
|
|
@ -1,5 +1,59 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IssuesPayload struct {
|
||||
Assigned []Issue
|
||||
Mentioned []Issue
|
||||
Recent []Issue
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
Labels []string
|
||||
TotalLabelCount int
|
||||
}
|
||||
|
||||
type apiIssues struct {
|
||||
Issues struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
Labels struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fragments = `
|
||||
fragment issue on Issue {
|
||||
number
|
||||
title
|
||||
labels(first: 3) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*Issue, error) {
|
||||
repoID, err := GitHubRepoId(client, ghRepo)
|
||||
if err != nil {
|
||||
|
|
@ -38,3 +92,156 @@ func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*I
|
|||
|
||||
return &result.CreateIssue.Issue, nil
|
||||
}
|
||||
|
||||
func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
|
||||
type response struct {
|
||||
Assigned apiIssues
|
||||
Mentioned apiIssues
|
||||
Recent apiIssues
|
||||
}
|
||||
|
||||
query := fragments + `
|
||||
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
|
||||
assigned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mentioned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recent: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {since: $since, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"viewer": currentUsername,
|
||||
"since": since,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assigned := convertAPIToIssues(resp.Assigned)
|
||||
mentioned := convertAPIToIssues(resp.Mentioned)
|
||||
recent := convertAPIToIssues(resp.Recent)
|
||||
|
||||
payload := IssuesPayload{
|
||||
assigned,
|
||||
mentioned,
|
||||
recent,
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func IssueList(client *Client, ghRepo Repo, state string, labels []string, assigneeString string, limit int) ([]Issue, error) {
|
||||
var states []string
|
||||
switch state {
|
||||
case "open", "":
|
||||
states = []string{"OPEN"}
|
||||
case "closed":
|
||||
states = []string{"CLOSED"}
|
||||
case "all":
|
||||
states = []string{"OPEN", "CLOSED"}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid state: %s", state)
|
||||
}
|
||||
|
||||
// If you don't want to filter by lables, graphql requires you need
|
||||
// to send nil instead of an empty array.
|
||||
if len(labels) == 0 {
|
||||
labels = nil
|
||||
}
|
||||
|
||||
var assignee interface{}
|
||||
if len(assigneeString) > 0 {
|
||||
assignee = assigneeString
|
||||
} else {
|
||||
assignee = nil
|
||||
}
|
||||
|
||||
query := fragments + `
|
||||
query($owner: String!, $repo: String!, $limit: Int, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issues(first: $limit, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
variables := map[string]interface{}{
|
||||
"limit": limit,
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
"assignee": assignee,
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Repository apiIssues
|
||||
}
|
||||
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issues := convertAPIToIssues(resp.Repository)
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func convertAPIToIssues(i apiIssues) []Issue {
|
||||
var issues []Issue
|
||||
for _, edge := range i.Issues.Edges {
|
||||
var labels []string
|
||||
for _, labelEdge := range edge.Node.Labels.Edges {
|
||||
labels = append(labels, labelEdge.Node.Name)
|
||||
}
|
||||
|
||||
issue := Issue{
|
||||
Number: edge.Node.Number,
|
||||
Title: edge.Node.Title,
|
||||
URL: edge.Node.URL,
|
||||
Labels: labels,
|
||||
TotalLabelCount: edge.Node.Labels.TotalCount,
|
||||
}
|
||||
issues = append(issues, issue)
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PullRequestsPayload struct {
|
||||
|
|
@ -18,34 +17,29 @@ type PullRequest struct {
|
|||
URL string
|
||||
HeadRefName string
|
||||
|
||||
IsCrossRepository bool
|
||||
HeadRepositoryOwner struct {
|
||||
Login string
|
||||
}
|
||||
|
||||
Reviews struct {
|
||||
Nodes []struct {
|
||||
State string
|
||||
Author struct {
|
||||
Login string
|
||||
}
|
||||
HeadRepository struct {
|
||||
Name string
|
||||
DefaultBranchRef struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
IsCrossRepository bool
|
||||
MaintainerCanModify bool
|
||||
|
||||
ReviewDecision string
|
||||
|
||||
Commits struct {
|
||||
Nodes []struct {
|
||||
Commit struct {
|
||||
Status struct {
|
||||
Contexts []struct {
|
||||
State string
|
||||
}
|
||||
}
|
||||
CheckSuites struct {
|
||||
Nodes []struct {
|
||||
CheckRuns struct {
|
||||
Nodes []struct {
|
||||
Conclusion string
|
||||
}
|
||||
StatusCheckRollup struct {
|
||||
Contexts struct {
|
||||
Nodes []struct {
|
||||
State string
|
||||
Status string
|
||||
Conclusion string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,23 +58,18 @@ func (pr PullRequest) HeadLabel() string {
|
|||
type PullRequestReviewStatus struct {
|
||||
ChangesRequested bool
|
||||
Approved bool
|
||||
ReviewRequired bool
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
|
||||
status := PullRequestReviewStatus{}
|
||||
reviewMap := map[string]string{}
|
||||
// Reviews will include every review on record, including consecutive ones
|
||||
// from the same actor. Consolidate them into latest state per reviewer.
|
||||
for _, review := range pr.Reviews.Nodes {
|
||||
reviewMap[review.Author.Login] = review.State
|
||||
}
|
||||
for _, state := range reviewMap {
|
||||
switch state {
|
||||
case "CHANGES_REQUESTED":
|
||||
status.ChangesRequested = true
|
||||
case "APPROVED":
|
||||
status.Approved = true
|
||||
}
|
||||
switch pr.ReviewDecision {
|
||||
case "CHANGES_REQUESTED":
|
||||
status.ChangesRequested = true
|
||||
case "APPROVED":
|
||||
status.Approved = true
|
||||
case "REVIEW_REQUIRED":
|
||||
status.ReviewRequired = true
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
|
@ -97,32 +86,28 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
|||
return
|
||||
}
|
||||
commit := pr.Commits.Nodes[0].Commit
|
||||
for _, status := range commit.Status.Contexts {
|
||||
switch status.State {
|
||||
case "SUCCESS":
|
||||
for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
|
||||
state := c.State // StatusContext
|
||||
if state == "" {
|
||||
// CheckRun
|
||||
if c.Status == "COMPLETED" {
|
||||
state = c.Conclusion
|
||||
} else {
|
||||
state = c.Status
|
||||
}
|
||||
}
|
||||
switch state {
|
||||
case "SUCCESS", "NEUTRAL", "SKIPPED":
|
||||
summary.Passing++
|
||||
case "EXPECTED", "ERROR", "FAILURE":
|
||||
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
|
||||
summary.Failing++
|
||||
case "PENDING":
|
||||
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS":
|
||||
summary.Pending++
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported status: %q", status.State))
|
||||
panic(fmt.Errorf("unsupported status: %q", state))
|
||||
}
|
||||
summary.Total++
|
||||
}
|
||||
for _, checkSuite := range commit.CheckSuites.Nodes {
|
||||
for _, checkRun := range checkSuite.CheckRuns.Nodes {
|
||||
switch checkRun.Conclusion {
|
||||
case "SUCCESS", "NEUTRAL":
|
||||
summary.Passing++
|
||||
case "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
|
||||
summary.Failing++
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported check conclusion: %q", checkRun.Conclusion))
|
||||
}
|
||||
summary.Total++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -131,109 +116,6 @@ type Repo interface {
|
|||
RepoOwner() string
|
||||
}
|
||||
|
||||
type IssuesPayload struct {
|
||||
Assigned []Issue
|
||||
Mentioned []Issue
|
||||
Recent []Issue
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
}
|
||||
|
||||
func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) {
|
||||
type issues struct {
|
||||
Issues struct {
|
||||
Edges []struct {
|
||||
Node Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Assigned issues
|
||||
Mentioned issues
|
||||
Recent issues
|
||||
}
|
||||
|
||||
query := `
|
||||
fragment issue on Issue {
|
||||
number
|
||||
title
|
||||
}
|
||||
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
|
||||
assigned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {assignee: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mentioned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {mentioned: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recent: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {since: $since}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"viewer": currentUsername,
|
||||
"since": since,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var assigned []Issue
|
||||
for _, edge := range resp.Assigned.Issues.Edges {
|
||||
assigned = append(assigned, edge.Node)
|
||||
}
|
||||
|
||||
var mentioned []Issue
|
||||
for _, edge := range resp.Mentioned.Issues.Edges {
|
||||
mentioned = append(mentioned, edge.Node)
|
||||
}
|
||||
|
||||
var recent []Issue
|
||||
for _, edge := range resp.Recent.Issues.Edges {
|
||||
recent = append(recent, edge.Node)
|
||||
}
|
||||
|
||||
payload := IssuesPayload{
|
||||
assigned,
|
||||
mentioned,
|
||||
recent,
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername string) (*PullRequestsPayload, error) {
|
||||
type edges struct {
|
||||
Edges []struct {
|
||||
|
|
@ -267,15 +149,14 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
status {
|
||||
contexts {
|
||||
state
|
||||
}
|
||||
}
|
||||
checkSuites(first: 50) {
|
||||
nodes {
|
||||
checkRuns(first: 50) {
|
||||
nodes {
|
||||
statusCheckRollup {
|
||||
contexts(last: 100) {
|
||||
nodes {
|
||||
...on StatusContext {
|
||||
state
|
||||
}
|
||||
...on CheckRun {
|
||||
status
|
||||
conclusion
|
||||
}
|
||||
}
|
||||
|
|
@ -287,16 +168,8 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
}
|
||||
fragment prWithReviews on PullRequest {
|
||||
...pr
|
||||
reviews(last: 20) {
|
||||
nodes {
|
||||
state
|
||||
author {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
reviewDecision
|
||||
}
|
||||
|
||||
query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(headRefName: $headRefName, states: OPEN, first: 1) {
|
||||
|
|
@ -374,6 +247,48 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
return &payload, nil
|
||||
}
|
||||
|
||||
func PullRequestByNumber(client *Client, ghRepo Repo, number int) (*PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest PullRequest
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
query($owner: String!, $repo: String!, $pr_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr_number) {
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
headRepository {
|
||||
name
|
||||
defaultBranchRef {
|
||||
name
|
||||
}
|
||||
}
|
||||
isCrossRepository
|
||||
maintainerCanModify
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": ghRepo.RepoOwner(),
|
||||
"repo": ghRepo.RepoName(),
|
||||
"pr_number": number,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
|
|
@ -19,7 +19,7 @@ func init() {
|
|||
&cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant issues",
|
||||
RunE: issueList,
|
||||
RunE: issueStatus,
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "view <issue-number>",
|
||||
|
|
@ -31,12 +31,23 @@ func init() {
|
|||
issueCmd.AddCommand(issueCreateCmd)
|
||||
issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue")
|
||||
|
||||
issueListCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List and filter issues in this repository",
|
||||
RunE: issueList,
|
||||
}
|
||||
issueListCmd.Flags().StringP("assignee", "a", "", "filter by assignee")
|
||||
issueListCmd.Flags().StringSliceP("label", "l", nil, "filter by label")
|
||||
issueListCmd.Flags().StringP("state", "s", "", "filter by state (open|closed|all)")
|
||||
issueListCmd.Flags().IntP("limit", "L", 30, "maximum number of issues to fetch")
|
||||
issueCmd.AddCommand((issueListCmd))
|
||||
}
|
||||
|
||||
var issueCmd = &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Work with GitHub issues",
|
||||
Long: `Helps you work with issues.`,
|
||||
Short: "Work with issues",
|
||||
Long: `Work with GitHub issues`,
|
||||
}
|
||||
var issueCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
|
|
@ -56,19 +67,65 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
state, err := cmd.Flags().GetString("state")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labels, err := cmd.Flags().GetStringSlice("label")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assignee, err := cmd.Flags().GetString("assignee")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limit, err := cmd.Flags().GetInt("limit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issues, err := api.IssueList(apiClient, baseRepo, state, labels, assignee, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(issues) > 0 {
|
||||
printIssues("", issues...)
|
||||
} else {
|
||||
message := fmt.Sprintf("There are no open issues")
|
||||
printMessage(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func issueStatus(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentUser, err := ctx.AuthLogin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issuePayload, err := api.Issues(apiClient, baseRepo, currentUser)
|
||||
issuePayload, err := api.IssueStatus(apiClient, baseRepo, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printHeader("Issues assigned to you")
|
||||
if issuePayload.Assigned != nil {
|
||||
printIssues(issuePayload.Assigned...)
|
||||
printIssues(" ", issuePayload.Assigned...)
|
||||
} else {
|
||||
message := fmt.Sprintf(" There are no issues assgined to you")
|
||||
printMessage(message)
|
||||
|
|
@ -77,7 +134,7 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
|
||||
printHeader("Issues mentioning you")
|
||||
if len(issuePayload.Mentioned) > 0 {
|
||||
printIssues(issuePayload.Mentioned...)
|
||||
printIssues(" ", issuePayload.Mentioned...)
|
||||
} else {
|
||||
printMessage(" There are no issues mentioning you")
|
||||
}
|
||||
|
|
@ -85,7 +142,7 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
|
||||
printHeader("Recent issues")
|
||||
if len(issuePayload.Recent) > 0 {
|
||||
printIssues(issuePayload.Recent...)
|
||||
printIssues(" ", issuePayload.Recent...)
|
||||
} else {
|
||||
printMessage(" There are no recent issues")
|
||||
}
|
||||
|
|
@ -185,8 +242,17 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func printIssues(issues ...api.Issue) {
|
||||
func printIssues(prefix string, issues ...api.Issue) {
|
||||
for _, issue := range issues {
|
||||
fmt.Printf(" #%d %s\n", issue.Number, truncate(70, issue.Title))
|
||||
number := utils.Green("#" + strconv.Itoa(issue.Number))
|
||||
var coloredLabels string
|
||||
if len(issue.Labels) > 0 {
|
||||
var ellipse string
|
||||
if issue.TotalLabelCount > len(issue.Labels) {
|
||||
ellipse = "…"
|
||||
}
|
||||
coloredLabels = utils.Gray(fmt.Sprintf(" (%s%s)", strings.Join(issue.Labels, ", "), ellipse))
|
||||
}
|
||||
fmt.Printf("%s%s %s %s\n", prefix, number, truncate(70, issue.Title), coloredLabels)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,11 +35,64 @@ func TestIssueStatus(t *testing.T) {
|
|||
|
||||
for _, r := range expectedIssues {
|
||||
if !r.MatchString(output) {
|
||||
t.Errorf("output did not match regexp /%s/", r)
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
||||
jsonFile, _ := os.Open("../test/fixtures/issueList.json")
|
||||
defer jsonFile.Close()
|
||||
http.StubResponse(200, jsonFile)
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "issue list")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
|
||||
expectedIssues := []*regexp.Regexp{
|
||||
regexp.MustCompile(`#1.*won`),
|
||||
regexp.MustCompile(`#2.*too`),
|
||||
regexp.MustCompile(`#4.*fore`),
|
||||
}
|
||||
|
||||
for _, r := range expectedIssues {
|
||||
if !r.MatchString(output) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_withFlags(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {}}`)) // Since we are testing that the flags are passed, we don't care about the response
|
||||
|
||||
_, err := test.RunCommand(RootCmd, "issue list -a probablyCher -l web,bug -s open")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Assignee string
|
||||
Labels []string
|
||||
States []string
|
||||
}
|
||||
}{}
|
||||
json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Assignee, "probablyCher")
|
||||
eq(t, reqBody.Variables.Labels, []string{"web", "bug"})
|
||||
eq(t, reqBody.Variables.States, []string{"OPEN"})
|
||||
}
|
||||
|
||||
func TestIssueView(t *testing.T) {
|
||||
initBlankContext("OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
|
|
|||
110
command/pr.go
110
command/pr.go
|
|
@ -2,15 +2,19 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/git"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(prCmd)
|
||||
prCmd.AddCommand(prCheckoutCmd)
|
||||
prCmd.AddCommand(prCreateCmd)
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prCmd.AddCommand(prStatusCmd)
|
||||
|
|
@ -25,11 +29,17 @@ func init() {
|
|||
var prCmd = &cobra.Command{
|
||||
Use: "pr",
|
||||
Short: "Work with pull requests",
|
||||
Long: `Helps you work with pull requests.`,
|
||||
Long: `Work with GitHub pull requests.`,
|
||||
}
|
||||
var prCheckoutCmd = &cobra.Command{
|
||||
Use: "checkout <pr-number>",
|
||||
Short: "Check out a pull request in Git",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: prCheckout,
|
||||
}
|
||||
var prListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pull requests",
|
||||
Short: "List and filter pull requests in this repository",
|
||||
RunE: prList,
|
||||
}
|
||||
var prStatusCmd = &cobra.Command{
|
||||
|
|
@ -222,6 +232,100 @@ func prView(cmd *cobra.Command, args []string) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
func prCheckout(cmd *cobra.Command, args []string) error {
|
||||
prNumber, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 := api.PullRequestByNumber(apiClient, baseRemote, prNumber)
|
||||
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", prNumber)
|
||||
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(prs ...api.PullRequest) {
|
||||
for _, pr := range prs {
|
||||
prNumber := fmt.Sprintf("#%d", pr.Number)
|
||||
|
|
@ -250,6 +354,8 @@ func printPrs(prs ...api.PullRequest) {
|
|||
|
||||
if reviews.ChangesRequested {
|
||||
fmt.Printf(" - %s", utils.Red("changes requested"))
|
||||
} else if reviews.ReviewRequired {
|
||||
fmt.Printf(" - %s", utils.Yellow("review required"))
|
||||
} else if reviews.Approved {
|
||||
fmt.Printf(" - %s", utils.Green("approved"))
|
||||
}
|
||||
|
|
|
|||
364
command/pr_checkout_test.go
Normal file
364
command/pr_checkout_test.go
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
func TestPRCheckout_sameRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
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":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track origin/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
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":
|
||||
return &outputStub{}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 3)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git merge --ff-only refs/remotes/origin/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
"robot-fork": "hubot/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
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":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch robot-fork +refs/heads/feature:refs/remotes/robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote robot-fork")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/pull/123/head")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &outputStub{[]byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &outputStub{[]byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git merge --ff-only FETCH_HEAD")
|
||||
}
|
||||
|
||||
func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote https://github.com/hubot/REPO.git")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
|
@ -68,14 +68,14 @@ func prCreate(cmd *cobra.Command, _ []string) error {
|
|||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "PR Title",
|
||||
Message: "Pull request title",
|
||||
Default: inProgress.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &survey.Editor{
|
||||
Message: fmt.Sprintf("PR Body (%s)", editor),
|
||||
Message: fmt.Sprintf("Pull request body (%s)", editor),
|
||||
FileName: "*.md",
|
||||
Default: inProgress.Body,
|
||||
AppendDefault: true,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ type FlagError struct {
|
|||
var RootCmd = &cobra.Command{
|
||||
Use: "gh",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Do things with GitHub from your terminal`,
|
||||
Long: `Work with GitHub from your terminal`,
|
||||
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
|
|
@ -75,6 +75,7 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
|||
// antiope-preview: Checks
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json, application/vnd.github.shadow-cat-preview"),
|
||||
api.AddHeader("GraphQL-Features", "pe_mobile"),
|
||||
}
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
opts = append(opts, api.VerboseLog(os.Stderr))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
)
|
||||
|
|
@ -34,3 +36,15 @@ func (s outputStub) Output() ([]byte, error) {
|
|||
func (s outputStub) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorStub struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (s errorStub) Output() ([]byte, error) {
|
||||
return nil, errors.New(s.message)
|
||||
}
|
||||
|
||||
func (s errorStub) Run() error {
|
||||
return errors.New(s.message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ func (r Remotes) FindByName(names ...string) (*Remote, error) {
|
|||
return nil, fmt.Errorf("no GitHub remotes found")
|
||||
}
|
||||
|
||||
// FindByRepo returns the first Remote that points to a specific GitHub repository
|
||||
func (r Remotes) FindByRepo(owner, name string) (*Remote, error) {
|
||||
for _, rem := range r {
|
||||
if strings.EqualFold(rem.RepoOwner(), owner) && strings.EqualFold(rem.RepoName(), name) {
|
||||
return rem, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no matching remote found")
|
||||
}
|
||||
|
||||
// Remote represents a git remote mapped to a GitHub repository
|
||||
type Remote struct {
|
||||
*git.Remote
|
||||
|
|
|
|||
30
git/git.go
30
git/git.go
|
|
@ -33,7 +33,6 @@ func Dir() (string, error) {
|
|||
|
||||
func WorkdirName() (string, error) {
|
||||
toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
toplevelCmd.Stderr = nil
|
||||
output, err := utils.PrepareCmd(toplevelCmd).Output()
|
||||
dir := firstLine(output)
|
||||
if dir == "" {
|
||||
|
|
@ -42,31 +41,10 @@ func WorkdirName() (string, error) {
|
|||
return dir, err
|
||||
}
|
||||
|
||||
func HasFile(segments ...string) bool {
|
||||
// The blessed way to resolve paths within git dir since Git 2.5.0
|
||||
pathCmd := exec.Command("git", "rev-parse", "-q", "--git-path", filepath.Join(segments...))
|
||||
if output, err := utils.PrepareCmd(pathCmd).Output(); err == nil {
|
||||
if lines := outputLines(output); len(lines) == 1 {
|
||||
if _, err := os.Stat(lines[0]); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for older git versions
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s := []string{dir}
|
||||
s = append(s, segments...)
|
||||
path := filepath.Join(s...)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
func VerifyRef(ref string) bool {
|
||||
showRef := exec.Command("git", "show-ref", "--verify", "--quiet", ref)
|
||||
err := utils.PrepareCmd(showRef).Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func BranchAtRef(paths ...string) (name string, err error) {
|
||||
|
|
|
|||
68
test/fixtures/issueList.json
vendored
68
test/fixtures/issueList.json
vendored
|
|
@ -1,47 +1,61 @@
|
|||
{
|
||||
"data": {
|
||||
"assigned": {
|
||||
"repository": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
"number": 1,
|
||||
"title": "number won",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mentioned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots"
|
||||
"number": 2,
|
||||
"title": "number too",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
"number": 4,
|
||||
"title": "number fore",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recent": {
|
||||
"issues": {
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ func (c cmdWithStderr) Output() ([]byte, error) {
|
|||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
if c.Cmd.Stderr != nil {
|
||||
return c.Cmd.Output()
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
out, err := c.Cmd.Output()
|
||||
|
|
@ -51,6 +54,9 @@ func (c cmdWithStderr) Run() error {
|
|||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
if c.Cmd.Stderr != nil {
|
||||
return c.Cmd.Run()
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
err := c.Cmd.Run()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue