X Tutup
Skip to content

Commit 4f6dfee

Browse files
committed
Merge remote-tracking branch 'origin/master' into pr-create-just-works-TM
2 parents f10b8d8 + c8d257c commit 4f6dfee

File tree

8 files changed

+161
-34
lines changed

8 files changed

+161
-34
lines changed

api/client.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"io/ioutil"
99
"net/http"
10+
"regexp"
1011
"strings"
1112
)
1213

@@ -35,13 +36,26 @@ func AddHeader(name, value string) ClientOption {
3536
}
3637

3738
// VerboseLog enables request/response logging within a RoundTripper
38-
func VerboseLog(out io.Writer) ClientOption {
39+
func VerboseLog(out io.Writer, logBodies bool) ClientOption {
3940
return func(tr http.RoundTripper) http.RoundTripper {
4041
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
4142
fmt.Fprintf(out, "> %s %s\n", req.Method, req.URL.RequestURI())
43+
if logBodies && req.Body != nil && inspectableMIMEType(req.Header.Get("Content-type")) {
44+
newBody := &bytes.Buffer{}
45+
io.Copy(out, io.TeeReader(req.Body, newBody))
46+
fmt.Fprintln(out)
47+
req.Body = ioutil.NopCloser(newBody)
48+
}
4249
res, err := tr.RoundTrip(req)
4350
if err == nil {
4451
fmt.Fprintf(out, "< HTTP %s\n", res.Status)
52+
if logBodies && res.Body != nil && inspectableMIMEType(res.Header.Get("Content-type")) {
53+
newBody := &bytes.Buffer{}
54+
// TODO: pretty-print response JSON
55+
io.Copy(out, io.TeeReader(res.Body, newBody))
56+
fmt.Fprintln(out)
57+
res.Body = ioutil.NopCloser(newBody)
58+
}
4559
}
4660
return res, err
4761
}}
@@ -193,3 +207,9 @@ func handleHTTPError(resp *http.Response) error {
193207

194208
return fmt.Errorf("http error, '%s' failed (%d): '%s'", resp.Request.URL, resp.StatusCode, message)
195209
}
210+
211+
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
212+
213+
func inspectableMIMEType(t string) bool {
214+
return strings.HasPrefix(t, "text/") || jsonTypeRE.MatchString(t)
215+
}

api/queries_issue.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"fmt"
5+
"time"
56
)
67

78
type IssuesPayload struct {
@@ -16,12 +17,13 @@ type IssuesAndTotalCount struct {
1617
}
1718

1819
type Issue struct {
19-
Number int
20-
Title string
21-
URL string
22-
State string
23-
Body string
24-
Comments struct {
20+
Number int
21+
Title string
22+
URL string
23+
State string
24+
Body string
25+
UpdatedAt time.Time
26+
Comments struct {
2527
TotalCount int
2628
}
2729
Author struct {
@@ -44,6 +46,7 @@ const fragments = `
4446
title
4547
url
4648
state
49+
updatedAt
4750
labels(first: 3) {
4851
nodes {
4952
name
@@ -111,19 +114,19 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
111114
query($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) {
112115
repository(owner: $owner, name: $repo) {
113116
hasIssuesEnabled
114-
assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
117+
assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
115118
totalCount
116119
nodes {
117120
...issue
118121
}
119122
}
120-
mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
123+
mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
121124
totalCount
122125
nodes {
123126
...issue
124127
}
125128
}
126-
authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
129+
authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
127130
totalCount
128131
nodes {
129132
...issue
@@ -233,13 +236,15 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig
233236
func IssueByNumber(client *Client, ghRepo Repo, number int) (*Issue, error) {
234237
type response struct {
235238
Repository struct {
236-
Issue Issue
239+
Issue Issue
240+
HasIssuesEnabled bool
237241
}
238242
}
239243

240244
query := `
241245
query($owner: String!, $repo: String!, $issue_number: Int!) {
242246
repository(owner: $owner, name: $repo) {
247+
hasIssuesEnabled
243248
issue(number: $issue_number) {
244249
title
245250
body
@@ -272,5 +277,9 @@ func IssueByNumber(client *Client, ghRepo Repo, number int) (*Issue, error) {
272277
return nil, err
273278
}
274279

280+
if !resp.Repository.HasIssuesEnabled {
281+
return nil, fmt.Errorf("the '%s/%s' repository has disabled issues", ghRepo.RepoOwner(), ghRepo.RepoName())
282+
}
283+
275284
return &resp.Repository.Issue, nil
276285
}

command/issue.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"regexp"
88
"strconv"
99
"strings"
10+
"time"
1011

1112
"github.com/github/gh-cli/api"
1213
"github.com/github/gh-cli/context"
@@ -385,7 +386,14 @@ func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue)
385386
if coloredLabels != "" {
386387
coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels))
387388
}
388-
fmt.Fprintf(w, "%s%s %s%s\n", prefix, number, truncate(70, replaceExcessiveWhitespace(issue.Title)), coloredLabels)
389+
390+
now := time.Now()
391+
ago := now.Sub(issue.UpdatedAt)
392+
393+
fmt.Fprintf(w, "%s%s %s%s %s\n", prefix, number,
394+
truncate(70, replaceExcessiveWhitespace(issue.Title)),
395+
coloredLabels,
396+
utils.Gray(utils.FuzzyAgo(ago)))
389397
}
390398
remaining := totalCount - len(issues)
391399
if remaining > 0 {

command/issue_test.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func TestIssueView(t *testing.T) {
203203
http := initFakeHTTP()
204204

205205
http.StubResponse(200, bytes.NewBufferString(`
206-
{ "data": { "repository": { "issue": {
206+
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
207207
"number": 123,
208208
"url": "https://github.com/OWNER/REPO/issues/123"
209209
} } } }
@@ -236,7 +236,7 @@ func TestIssueView_preview(t *testing.T) {
236236
http := initFakeHTTP()
237237

238238
http.StubResponse(200, bytes.NewBufferString(`
239-
{ "data": { "repository": { "issue": {
239+
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
240240
"number": 123,
241241
"body": "**bold story**",
242242
"title": "ix of coins",
@@ -303,12 +303,29 @@ func TestIssueView_notFound(t *testing.T) {
303303
}
304304
}
305305

306+
func TestIssueView_disabledIssues(t *testing.T) {
307+
initBlankContext("OWNER/REPO", "master")
308+
http := initFakeHTTP()
309+
310+
http.StubResponse(200, bytes.NewBufferString(`
311+
{ "data": { "repository": {
312+
"id": "REPOID",
313+
"hasIssuesEnabled": false
314+
} } }
315+
`))
316+
317+
_, err := RunCommand(issueViewCmd, `issue view 6666`)
318+
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
319+
t.Errorf("error running command `issue view`: %v", err)
320+
}
321+
}
322+
306323
func TestIssueView_urlArg(t *testing.T) {
307324
initBlankContext("OWNER/REPO", "master")
308325
http := initFakeHTTP()
309326

310327
http.StubResponse(200, bytes.NewBufferString(`
311-
{ "data": { "repository": { "issue": {
328+
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
312329
"number": 123,
313330
"url": "https://github.com/OWNER/REPO/issues/123"
314331
} } } }

command/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func BasicClient() (*api.Client, error) {
8484
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", c.Token)))
8585
}
8686
if verbose := os.Getenv("DEBUG"); verbose != "" {
87-
opts = append(opts, api.VerboseLog(os.Stderr))
87+
opts = append(opts, api.VerboseLog(os.Stderr, false))
8888
}
8989
return api.NewClient(opts...), nil
9090
}
@@ -112,7 +112,7 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
112112
api.AddHeader("GraphQL-Features", "pe_mobile"),
113113
}
114114
if verbose := os.Getenv("DEBUG"); verbose != "" {
115-
opts = append(opts, api.VerboseLog(os.Stderr))
115+
opts = append(opts, api.VerboseLog(os.Stderr, strings.Contains(verbose, "api")))
116116
}
117117
return api.NewClient(opts...), nil
118118
}

context/config_success.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,37 @@ const oauthSuccessPage = `
66
<title>Success: GitHub CLI</title>
77
<style type="text/css">
88
body {
9-
color: #333;
10-
font-size: 14px;
11-
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
12-
line-height: 1.5;
13-
max-width: 461px;
14-
margin: 2em auto;
15-
text-align: center;
9+
color: #1B1F23;
10+
background: #F6F8FA;
11+
font-size: 14px;
12+
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
13+
line-height: 1.5;
14+
max-width: 620px;
15+
margin: 28px auto;
16+
text-align: center;
1617
}
18+
1719
h1 {
18-
color: #555;
19-
font-size: 22px;
20-
letter-spacing: 1px;
20+
font-size: 24px;
21+
margin-bottom: 0;
22+
}
23+
24+
p {
25+
margin-top: 0;
2126
}
22-
</style>
2327
28+
.box {
29+
border: 1px solid #E1E4E8;
30+
background: white;
31+
padding: 24px;
32+
margin: 28px;
33+
}
34+
</style>
2435
<body>
25-
<h1>Authentication successful.</h1>
26-
<p>
27-
You have completed logging into GitHub CLI.<br>
28-
You may now <strong>close this tab and return to the terminal</strong>.
29-
</p>
30-
<img alt="" src="https://octodex.github.com/images/daftpunktocat-guy.gif" height="461">
36+
<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>
37+
<div class="box">
38+
<h1>Successfully authenticated GitHub CLI</h1>
39+
<p>You may now close this tab and return to the terminal.</p>
40+
</div>
3141
</body>
3242
`

utils/utils.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"runtime"
10+
"time"
1011

1112
"github.com/kballard/go-shellquote"
1213
md "github.com/vilmibm/go-termd"
@@ -90,3 +91,27 @@ func Pluralize(num int, thing string) string {
9091
return fmt.Sprintf("%d %ss", num, thing)
9192
}
9293
}
94+
95+
func fmtDuration(amount int, unit string) string {
96+
return fmt.Sprintf("about %s ago", Pluralize(amount, unit))
97+
}
98+
99+
func FuzzyAgo(ago time.Duration) string {
100+
if ago < time.Minute {
101+
return "less than a minute ago"
102+
}
103+
if ago < time.Hour {
104+
return fmtDuration(int(ago.Minutes()), "minute")
105+
}
106+
if ago < 24*time.Hour {
107+
return fmtDuration(int(ago.Hours()), "hour")
108+
}
109+
if ago < 30*24*time.Hour {
110+
return fmtDuration(int(ago.Hours())/24, "day")
111+
}
112+
if ago < 365*24*time.Hour {
113+
return fmtDuration(int(ago.Hours())/24/30, "month")
114+
}
115+
116+
return fmtDuration(int(ago.Hours()/24/365), "year")
117+
}

utils/utils_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package utils
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestFuzzyAgo(t *testing.T) {
9+
10+
cases := map[string]string{
11+
"1s": "less than a minute ago",
12+
"30s": "less than a minute ago",
13+
"1m08s": "about 1 minute ago",
14+
"15m0s": "about 15 minutes ago",
15+
"59m10s": "about 59 minutes ago",
16+
"1h10m02s": "about 1 hour ago",
17+
"15h0m01s": "about 15 hours ago",
18+
"30h10m": "about 1 day ago",
19+
"50h": "about 2 days ago",
20+
"720h05m": "about 1 month ago",
21+
"3000h10m": "about 4 months ago",
22+
"8760h59m": "about 1 year ago",
23+
"17601h59m": "about 2 years ago",
24+
"262800h19m": "about 30 years ago",
25+
}
26+
27+
for duration, expected := range cases {
28+
d, e := time.ParseDuration(duration)
29+
if e != nil {
30+
t.Errorf("failed to create a duration: %s", e)
31+
}
32+
33+
fuzzy := FuzzyAgo(d)
34+
if fuzzy != expected {
35+
t.Errorf("unexpected fuzzy duration value: %s for %s", fuzzy, duration)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)
X Tutup