Merge remote-tracking branch 'origin/master' into pr-create-just-works-TM

This commit is contained in:
Mislav Marohnić 2020-01-23 14:08:07 +01:00
commit 4f6dfee965
8 changed files with 161 additions and 34 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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"
} } } }

View file

@ -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
}

View file

@ -6,27 +6,37 @@ const oauthSuccessPage = `
<title>Success: GitHub CLI</title>
<style type="text/css">
body {
color: #333;
font-size: 14px;
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.5;
max-width: 461px;
margin: 2em auto;
text-align: center;
color: #1B1F23;
background: #F6F8FA;
font-size: 14px;
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
line-height: 1.5;
max-width: 620px;
margin: 28px auto;
text-align: center;
}
h1 {
color: #555;
font-size: 22px;
letter-spacing: 1px;
font-size: 24px;
margin-bottom: 0;
}
p {
margin-top: 0;
}
.box {
border: 1px solid #E1E4E8;
background: white;
padding: 24px;
margin: 28px;
}
</style>
<body>
<h1>Authentication successful.</h1>
<p>
You have completed logging into GitHub CLI.<br>
You may now <strong>close this tab and return to the terminal</strong>.
</p>
<img alt="" src="https://octodex.github.com/images/daftpunktocat-guy.gif" height="461">
<svg height="52" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="52" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
<div class="box">
<h1>Successfully authenticated GitHub CLI</h1>
<p>You may now close this tab and return to the terminal.</p>
</div>
</body>
`

View file

@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"runtime"
"time"
"github.com/kballard/go-shellquote"
md "github.com/vilmibm/go-termd"
@ -90,3 +91,27 @@ func Pluralize(num int, thing string) string {
return fmt.Sprintf("%d %ss", num, thing)
}
}
func fmtDuration(amount int, unit string) string {
return fmt.Sprintf("about %s ago", Pluralize(amount, unit))
}
func FuzzyAgo(ago time.Duration) string {
if ago < time.Minute {
return "less than a minute ago"
}
if ago < time.Hour {
return fmtDuration(int(ago.Minutes()), "minute")
}
if ago < 24*time.Hour {
return fmtDuration(int(ago.Hours()), "hour")
}
if ago < 30*24*time.Hour {
return fmtDuration(int(ago.Hours())/24, "day")
}
if ago < 365*24*time.Hour {
return fmtDuration(int(ago.Hours())/24/30, "month")
}
return fmtDuration(int(ago.Hours()/24/365), "year")
}

38
utils/utils_test.go Normal file
View file

@ -0,0 +1,38 @@
package utils
import (
"testing"
"time"
)
func TestFuzzyAgo(t *testing.T) {
cases := map[string]string{
"1s": "less than a minute ago",
"30s": "less than a minute ago",
"1m08s": "about 1 minute ago",
"15m0s": "about 15 minutes ago",
"59m10s": "about 59 minutes ago",
"1h10m02s": "about 1 hour ago",
"15h0m01s": "about 15 hours ago",
"30h10m": "about 1 day ago",
"50h": "about 2 days ago",
"720h05m": "about 1 month ago",
"3000h10m": "about 4 months ago",
"8760h59m": "about 1 year ago",
"17601h59m": "about 2 years ago",
"262800h19m": "about 30 years ago",
}
for duration, expected := range cases {
d, e := time.ParseDuration(duration)
if e != nil {
t.Errorf("failed to create a duration: %s", e)
}
fuzzy := FuzzyAgo(d)
if fuzzy != expected {
t.Errorf("unexpected fuzzy duration value: %s for %s", fuzzy, duration)
}
}
}