diff --git a/api/client.go b/api/client.go index f3100815d..c056680c9 100644 --- a/api/client.go +++ b/api/client.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "regexp" "strings" ) @@ -35,13 +36,26 @@ func AddHeader(name, value string) ClientOption { } // VerboseLog enables request/response logging within a RoundTripper -func VerboseLog(out io.Writer) ClientOption { +func VerboseLog(out io.Writer, logBodies bool) ClientOption { return func(tr http.RoundTripper) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { fmt.Fprintf(out, "> %s %s\n", req.Method, req.URL.RequestURI()) + if logBodies && req.Body != nil && inspectableMIMEType(req.Header.Get("Content-type")) { + newBody := &bytes.Buffer{} + io.Copy(out, io.TeeReader(req.Body, newBody)) + fmt.Fprintln(out) + req.Body = ioutil.NopCloser(newBody) + } res, err := tr.RoundTrip(req) if err == nil { fmt.Fprintf(out, "< HTTP %s\n", res.Status) + if logBodies && res.Body != nil && inspectableMIMEType(res.Header.Get("Content-type")) { + newBody := &bytes.Buffer{} + // TODO: pretty-print response JSON + io.Copy(out, io.TeeReader(res.Body, newBody)) + fmt.Fprintln(out) + res.Body = ioutil.NopCloser(newBody) + } } return res, err }} @@ -193,3 +207,9 @@ func handleHTTPError(resp *http.Response) error { return fmt.Errorf("http error, '%s' failed (%d): '%s'", resp.Request.URL, resp.StatusCode, message) } + +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +func inspectableMIMEType(t string) bool { + return strings.HasPrefix(t, "text/") || jsonTypeRE.MatchString(t) +} diff --git a/api/queries_issue.go b/api/queries_issue.go index 39a998199..50fcbd434 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "time" ) type IssuesPayload struct { @@ -16,12 +17,13 @@ type IssuesAndTotalCount struct { } type Issue struct { - Number int - Title string - URL string - State string - Body string - Comments struct { + Number int + Title string + URL string + State string + Body string + UpdatedAt time.Time + Comments struct { TotalCount int } Author struct { @@ -44,6 +46,7 @@ const fragments = ` title url state + updatedAt labels(first: 3) { nodes { name @@ -111,19 +114,19 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa query($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) { repository(owner: $owner, name: $repo) { hasIssuesEnabled - assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { + assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { ...issue } } - mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { + mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { ...issue } } - authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) { + authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { ...issue @@ -233,13 +236,15 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig func IssueByNumber(client *Client, ghRepo Repo, number int) (*Issue, error) { type response struct { Repository struct { - Issue Issue + Issue Issue + HasIssuesEnabled bool } } query := ` query($owner: String!, $repo: String!, $issue_number: Int!) { repository(owner: $owner, name: $repo) { + hasIssuesEnabled issue(number: $issue_number) { title body @@ -272,5 +277,9 @@ func IssueByNumber(client *Client, ghRepo Repo, number int) (*Issue, error) { return nil, err } + if !resp.Repository.HasIssuesEnabled { + return nil, fmt.Errorf("the '%s/%s' repository has disabled issues", ghRepo.RepoOwner(), ghRepo.RepoName()) + } + return &resp.Repository.Issue, nil } diff --git a/command/issue.go b/command/issue.go index 63974cb20..139ab3337 100644 --- a/command/issue.go +++ b/command/issue.go @@ -7,6 +7,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" @@ -385,7 +386,14 @@ func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) if coloredLabels != "" { coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels)) } - fmt.Fprintf(w, "%s%s %s%s\n", prefix, number, truncate(70, replaceExcessiveWhitespace(issue.Title)), coloredLabels) + + now := time.Now() + ago := now.Sub(issue.UpdatedAt) + + fmt.Fprintf(w, "%s%s %s%s %s\n", prefix, number, + truncate(70, replaceExcessiveWhitespace(issue.Title)), + coloredLabels, + utils.Gray(utils.FuzzyAgo(ago))) } remaining := totalCount - len(issues) if remaining > 0 { diff --git a/command/issue_test.go b/command/issue_test.go index e9f616f68..9cf4156b4 100644 --- a/command/issue_test.go +++ b/command/issue_test.go @@ -203,7 +203,7 @@ func TestIssueView(t *testing.T) { http := initFakeHTTP() http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { "issue": { + { "data": { "repository": { "hasIssuesEnabled": true, "issue": { "number": 123, "url": "https://github.com/OWNER/REPO/issues/123" } } } } @@ -236,7 +236,7 @@ func TestIssueView_preview(t *testing.T) { http := initFakeHTTP() http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { "issue": { + { "data": { "repository": { "hasIssuesEnabled": true, "issue": { "number": 123, "body": "**bold story**", "title": "ix of coins", @@ -303,12 +303,29 @@ func TestIssueView_notFound(t *testing.T) { } } +func TestIssueView_disabledIssues(t *testing.T) { + initBlankContext("OWNER/REPO", "master") + http := initFakeHTTP() + + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "repository": { + "id": "REPOID", + "hasIssuesEnabled": false + } } } + `)) + + _, err := RunCommand(issueViewCmd, `issue view 6666`) + if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" { + t.Errorf("error running command `issue view`: %v", err) + } +} + func TestIssueView_urlArg(t *testing.T) { initBlankContext("OWNER/REPO", "master") http := initFakeHTTP() http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { "issue": { + { "data": { "repository": { "hasIssuesEnabled": true, "issue": { "number": 123, "url": "https://github.com/OWNER/REPO/issues/123" } } } } diff --git a/command/root.go b/command/root.go index f2bfab638..c945d02e8 100644 --- a/command/root.go +++ b/command/root.go @@ -84,7 +84,7 @@ func BasicClient() (*api.Client, error) { opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", c.Token))) } if verbose := os.Getenv("DEBUG"); verbose != "" { - opts = append(opts, api.VerboseLog(os.Stderr)) + opts = append(opts, api.VerboseLog(os.Stderr, false)) } return api.NewClient(opts...), nil } @@ -112,7 +112,7 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) { api.AddHeader("GraphQL-Features", "pe_mobile"), } if verbose := os.Getenv("DEBUG"); verbose != "" { - opts = append(opts, api.VerboseLog(os.Stderr)) + opts = append(opts, api.VerboseLog(os.Stderr, strings.Contains(verbose, "api"))) } return api.NewClient(opts...), nil } diff --git a/context/config_success.go b/context/config_success.go index 3a6fe978b..1919fc1a4 100644 --- a/context/config_success.go +++ b/context/config_success.go @@ -6,27 +6,37 @@ const oauthSuccessPage = `
- You have completed logging into GitHub CLI.
- You may now close this tab and return to the terminal.
-
+
+ You may now close this tab and return to the terminal.
+