Merge remote-tracking branch 'origin/master' into pr-create-just-works-TM
This commit is contained in:
commit
4f6dfee965
8 changed files with 161 additions and 34 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
} } } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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
38
utils/utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue