Merge pull request #5369 from cli/gh-status
This commit is contained in:
commit
e2fd7ceff0
11 changed files with 1518 additions and 20 deletions
|
|
@ -316,41 +316,55 @@ func graphQLClient(h *http.Client, hostname string) *graphql.Client {
|
|||
|
||||
// REST performs a REST request and parses the response.
|
||||
func (c Client) REST(hostname string, method string, p string, body io.Reader, data interface{}) error {
|
||||
_, err := c.RESTWithNext(hostname, method, p, body, data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c Client) RESTWithNext(hostname string, method string, p string, body io.Reader, data interface{}) (string, error) {
|
||||
req, err := http.NewRequest(method, restURL(hostname, p), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
if !success {
|
||||
return HandleHTTPError(resp)
|
||||
return "", HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return nil
|
||||
var next string
|
||||
for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) {
|
||||
if len(m) > 2 && m[2] == "next" {
|
||||
next = m[1]
|
||||
}
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`)
|
||||
|
||||
func restURL(hostname string, pathOrURL string) string {
|
||||
if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") {
|
||||
return pathOrURL
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -7,6 +7,7 @@ require (
|
|||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/charmbracelet/glamour v0.4.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/cli/browser v1.1.0
|
||||
github.com/cli/oauth v0.9.0
|
||||
github.com/cli/safeexec v1.0.0
|
||||
|
|
@ -27,7 +28,7 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.9.0
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b
|
||||
|
|
|
|||
7
go.sum
7
go.sum
|
|
@ -48,6 +48,8 @@ github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/pp
|
|||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/charmbracelet/glamour v0.4.0 h1:scR+smyB7WdmrlIaff6IVlm48P48JaNM7JypM/VGl4k=
|
||||
github.com/charmbracelet/glamour v0.4.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
|
|
@ -182,6 +184,7 @@ github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
|||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
|
|
@ -190,10 +193,12 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
|
|||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
searchCmd "github.com/cli/cli/v2/pkg/cmd/search"
|
||||
secretCmd "github.com/cli/cli/v2/pkg/cmd/secret"
|
||||
sshKeyCmd "github.com/cli/cli/v2/pkg/cmd/ssh-key"
|
||||
statusCmd "github.com/cli/cli/v2/pkg/cmd/status"
|
||||
versionCmd "github.com/cli/cli/v2/pkg/cmd/version"
|
||||
workflowCmd "github.com/cli/cli/v2/pkg/cmd/workflow"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -83,6 +84,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(searchCmd.NewCmdSearch(f))
|
||||
cmd.AddCommand(secretCmd.NewCmdSecret(f))
|
||||
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
|
||||
cmd.AddCommand(statusCmd.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(newCodespaceCmd(f))
|
||||
|
||||
// the `api` command should not inherit any extra HTTP headers
|
||||
|
|
|
|||
188
pkg/cmd/status/fixtures/events.json
Normal file
188
pkg/cmd/status/fixtures/events.json
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
[
|
||||
{
|
||||
"type": "PullRequestEvent",
|
||||
"repo": {
|
||||
"name": "rpd/todo"
|
||||
},
|
||||
"payload": {
|
||||
"action": "opened",
|
||||
"number": 5326,
|
||||
"pull_request": {
|
||||
"number": 5326,
|
||||
"title": "Only write UTF-8 BOM on Windows where it is needed"
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-17T15:42:28Z",
|
||||
"org": {
|
||||
"login": "rpd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "IssueCommentEvent",
|
||||
"repo": {
|
||||
"name": "vilmibm/testing"
|
||||
},
|
||||
"payload": {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"number": 5325,
|
||||
"title": "Ability to search \"not\" thing"
|
||||
},
|
||||
"comment": {
|
||||
"html_url": "https://github.com/vilmibm/testing/issues/5325#issuecomment-1070868636",
|
||||
"body": "We are working on dedicated `search` functionality and might be exploring some syntax for denoting \"NOT\" qualifiers. However, until something like this is added, you can always make \"NOT\" searches by using the `--search` flag:\r\n```\r\ngh pr list --search \"-author:app/dependabot\"\r\n```\r\n/cc @samcoe "
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-17T12:36:31Z"
|
||||
},
|
||||
{
|
||||
"type": "DeleteEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"ref": "dependabot/go_modules/github.com/stretchr/testify-1.7.1",
|
||||
"ref_type": "branch",
|
||||
"pusher_type": "user"
|
||||
},
|
||||
"created_at": "2022-03-16T14:41:30Z",
|
||||
"org": {
|
||||
"login": "cli",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59704711?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"ref": "dependabot/go_modules/github.com/stretchr/testify-1.7.1",
|
||||
"ref_type": "branch",
|
||||
"master_branch": "trunk",
|
||||
"description": "GitHub’s official command line tool",
|
||||
"pusher_type": "user"
|
||||
},
|
||||
"created_at": "2022-03-16T14:25:19Z",
|
||||
"org": {
|
||||
"login": "cli",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59704711?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "PullRequestReviewCommentEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"action": "created",
|
||||
"comment": {
|
||||
"body": "Wondering if we shouldn't name this something more generic and use it everywhere else to DRY things up a bit?",
|
||||
"html_url": "https://github.com/cli/cli/pull/5319#discussion_r827935007"
|
||||
},
|
||||
"pull_request": {
|
||||
"number": 5319,
|
||||
"title": "[Codespaces] Disallow some operations on codespaces that have a pending operation"
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-16T12:08:02Z",
|
||||
"org": {
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CommitCommentEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"comment": {
|
||||
"html_url": "https://github.com/cli/cli/commit/1b50852b2dfecd17ca5d3a2a12eb5c16df9fe46b#r68794505",
|
||||
"body": "spam"
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-16T03:32:59Z",
|
||||
"org": {
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "PushEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"push_id": 9359833428,
|
||||
"size": 1,
|
||||
"distinct_size": 1,
|
||||
"ref": "refs/heads/jg/event-handling",
|
||||
"head": "7d07249150fe1a24b2b49b3ff7c55e2152446a5e",
|
||||
"before": "06f1f6eb527a34af1222ca03807a9205a17d6e90",
|
||||
"commits": [
|
||||
{
|
||||
"sha": "7d07249150fe1a24b2b49b3ff7c55e2152446a5e",
|
||||
"author": {
|
||||
"email": "68619889+bchuecos@users.noreply.github.com",
|
||||
"name": "Bernardo"
|
||||
},
|
||||
"message": "review suggestions",
|
||||
"distinct": true,
|
||||
"url": "https://api.github.com/repos/cli/cli/commits/7d07249150fe1a24b2b49b3ff7c55e2152446a5e"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": "2022-03-15T18:22:41Z",
|
||||
"org": {
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WatchEvent",
|
||||
"repo": {
|
||||
"name": "fengari-lua/fengari",
|
||||
"url": "https://api.github.com/repos/fengari-lua/fengari"
|
||||
},
|
||||
"payload": {
|
||||
"action": "started"
|
||||
},
|
||||
"created_at": "2022-03-14T13:03:51Z",
|
||||
"org": {
|
||||
"login": "fengari-lua",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28658472?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "IssuesEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"action": "closed",
|
||||
"issue": {
|
||||
"number": 5301,
|
||||
"title": "wheee"
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-14T12:21:59Z",
|
||||
"org": {
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "IssuesEvent",
|
||||
"repo": {
|
||||
"name": "cli/cli"
|
||||
},
|
||||
"payload": {
|
||||
"action": "opened",
|
||||
"issue": {
|
||||
"number": 5300,
|
||||
"title": "Terminal bell when a running task is done"
|
||||
}
|
||||
},
|
||||
"created_at": "2022-03-14T12:21:59Z",
|
||||
"org": {
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
]
|
||||
138
pkg/cmd/status/fixtures/notifications.json
Normal file
138
pkg/cmd/status/fixtures/notifications.json
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
[
|
||||
{
|
||||
"reason": "team_mention",
|
||||
"subject": {
|
||||
"title": "Tracking: Close an issue as either resolved or unresolved",
|
||||
"url": "https://api.github.com/repos/vilmibm/testing/issues/594",
|
||||
"latest_comment_url": "https://api.github.com/repos/vilmibm/testing/issues/comments/1070788734",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "vilmibm/testing",
|
||||
"owner": {
|
||||
"login": "vilmibm"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "comment",
|
||||
"subject": {
|
||||
"title": "Unclear message about base repository when creating a PR",
|
||||
"url": "https://api.github.com/repos/cli/cli/issues/2090",
|
||||
"latest_comment_url": "https://api.github.com/repos/cli/cli/issues/comments/1069309546",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "cli/cli",
|
||||
"owner": {
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "mention",
|
||||
"subject": {
|
||||
"title": "Good",
|
||||
"url": "https://api.github.com/repos/rpd/todo/issues/110",
|
||||
"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/110",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "rpd/todo",
|
||||
"owner": {
|
||||
"login": "rpd"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "mention",
|
||||
"subject": {
|
||||
"title": "Fire Irons",
|
||||
"url": "https://api.github.com/repos/rpd/todo/issues/4113",
|
||||
"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/4113",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "rpd/todo",
|
||||
"owner": {
|
||||
"login": "rpd"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "mention",
|
||||
"subject": {
|
||||
"title": "PR merge should switch to the base branch instead of the default branch.",
|
||||
"url": "https://api.github.com/repos/cli/cli/issues/1096",
|
||||
"latest_comment_url": "https://api.github.com/repos/cli/cli/issues/1096",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "cli/cli",
|
||||
"owner": {
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "mention",
|
||||
"subject": {
|
||||
"title": "Find bolt cutter",
|
||||
"url": "https://api.github.com/repos/rpd/todo/issues/116",
|
||||
"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/comments/1065",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "rpd/todo",
|
||||
"owner": {
|
||||
"login": "rpd"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "mention",
|
||||
"subject": {
|
||||
"title": "use the action to precompile",
|
||||
"url": "https://api.github.com/repos/vilmibm/gh-screensaver/issues/15",
|
||||
"latest_comment_url": "https://api.github.com/repos/vilmibm/gh-screensaver/issues/comments/10",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "vilmibm/gh-screensaver",
|
||||
"owner": {
|
||||
"login": "vilmibm"
|
||||
},
|
||||
"url": "https://api.github.com/repos/vilmibm/gh-screensaver"
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "author",
|
||||
"subject": {
|
||||
"title": "asodijasodji",
|
||||
"url": "https://api.github.com/repos/vilmibm/testing/issues/150",
|
||||
"latest_comment_url": "https://api.github.com/repos/vilmibm/testing/issues/comments/1064534165",
|
||||
"type": "Issue"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "vilmibm/testing",
|
||||
"owner": {
|
||||
"login": "vilmibm"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"reason": "review_requested",
|
||||
"subject": {
|
||||
"title": "Update LICENSE",
|
||||
"url": "https://api.github.com/repos/cli/cli/pulls/5277",
|
||||
"latest_comment_url": null,
|
||||
"type": "PullRequest"
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "cli/cli",
|
||||
"owner": {
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
188
pkg/cmd/status/fixtures/search.json
Normal file
188
pkg/cmd/status/fixtures/search.json
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
{
|
||||
"data": {
|
||||
"assignments": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-03-15T17:10:25Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Pin extensions",
|
||||
"number": 5272,
|
||||
"repository": {
|
||||
"nameWithOwner": "cli/cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-03-01T17:10:25Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Board up RPD windows",
|
||||
"number": 73,
|
||||
"repository": {
|
||||
"nameWithOwner": "rpd/todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2022-03-10T21:33:57Z",
|
||||
"title": "yolo",
|
||||
"number": 157,
|
||||
"repository": {
|
||||
"nameWithOwner": "vilmibm/testing"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-01-06T05:05:12Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Issue Frecency",
|
||||
"number": 4768,
|
||||
"repository": {
|
||||
"nameWithOwner": "cli/cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2021-08-05T20:03:07Z",
|
||||
"title": "Reducing zombie threat in Raccoon City",
|
||||
"number": 514,
|
||||
"repository": {
|
||||
"nameWithOwner": "rpd/todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2021-10-16T23:20:35Z",
|
||||
"title": "Repo garden for Windows",
|
||||
"number": 3223,
|
||||
"repository": {
|
||||
"nameWithOwner": "cli/cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2020-11-10T23:07:37Z",
|
||||
"title": "welp",
|
||||
"number": 74,
|
||||
"repository": {
|
||||
"nameWithOwner": "vilmibm/testing"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2019-07-20T01:30:01Z",
|
||||
"title": "Ability to send emails from helpdesk UI",
|
||||
"number": 42,
|
||||
"repository": {
|
||||
"nameWithOwner": "tildetown/tildetown-admin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2019-07-22T19:37:45Z",
|
||||
"title": "complete move to class based views",
|
||||
"number": 22,
|
||||
"repository": {
|
||||
"nameWithOwner": "adreyer/arkestrator"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2019-02-20T17:54:24Z",
|
||||
"title": "`(every)` macro",
|
||||
"number": 145,
|
||||
"repository": {
|
||||
"nameWithOwner": "vilmibm/tildemush"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"__typename": "Issue",
|
||||
"updatedAt": "2019-02-20T18:10:53Z",
|
||||
"title": "audit move rules",
|
||||
"number": 79,
|
||||
"repository": {
|
||||
"nameWithOwner": "vilmibm/tildemush"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewRequested": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-03-15T17:10:25Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Pin extensions",
|
||||
"number": 5272,
|
||||
"repository": {
|
||||
"nameWithOwner": "cli/cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-02-18T19:38:20Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Foobar",
|
||||
"number": 1234,
|
||||
"repository": {
|
||||
"nameWithOwner": "vilmibm/testing"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2022-02-04T06:56:48Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Welcome party for Leon",
|
||||
"number": 50,
|
||||
"repository": {
|
||||
"nameWithOwner": "rpd/todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2021-12-20T10:30:39Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "Haircut for Leon",
|
||||
"number": 49,
|
||||
"repository": {
|
||||
"nameWithOwner": "rpd/todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"updatedAt": "2021-12-20T16:43:15Z",
|
||||
"__typename": "PullRequest",
|
||||
"title": "This pull request adds support for json output the to `gh pr checks` …",
|
||||
"number": 4671,
|
||||
"repository": {
|
||||
"nameWithOwner": "cli/cli"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
619
pkg/cmd/status/status.go
Normal file
619
pkg/cmd/status/status.go
Normal file
|
|
@ -0,0 +1,619 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type StatusOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
CachedClient func(*http.Client, time.Duration) *http.Client
|
||||
IO *iostreams.IOStreams
|
||||
Org string
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command {
|
||||
opts := &StatusOptions{
|
||||
CachedClient: func(c *http.Client, ttl time.Duration) *http.Client {
|
||||
return api.NewCachedClient(c, ttl)
|
||||
},
|
||||
}
|
||||
opts.HttpClient = f.HttpClient
|
||||
opts.IO = f.IOStreams
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Print information about relevant issues, pull requests, and notifications across repositories",
|
||||
Long: heredoc.Doc(`
|
||||
The status command prints information about your work on GitHub across all the repositories you're subscribed to, including:
|
||||
|
||||
- Assigned Issues
|
||||
- Assigned Pull Requests
|
||||
- Review Requests
|
||||
- Mentions
|
||||
- Repository Activity (new issues/pull requests, comments)
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
$ gh status -e cli/cli -e cli/go-gh # Exclude multiple repositories
|
||||
$ gh status -o cli # Limit results to a single organization
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return statusRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Org, "org", "o", "", "Report status within an organization")
|
||||
cmd.Flags().StringSliceVarP(&opts.Exclude, "exclude", "e", []string{}, "Comma separated list of repos to exclude in owner/name format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Reason string
|
||||
Subject struct {
|
||||
Title string
|
||||
LatestCommentURL string `json:"latest_comment_url"`
|
||||
URL string
|
||||
Type string
|
||||
}
|
||||
Repository struct {
|
||||
Owner struct {
|
||||
Login string
|
||||
}
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
}
|
||||
|
||||
type StatusItem struct {
|
||||
Repository string // owner/repo
|
||||
Identifier string // eg cli/cli#1234 or just 1234
|
||||
preview string // eg This is the truncated body of something...
|
||||
Reason string // only used in repo activity
|
||||
}
|
||||
|
||||
func (s StatusItem) Preview() string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(s.preview, "\r", ""), "\n", " ")
|
||||
}
|
||||
|
||||
type IssueOrPR struct {
|
||||
Number int
|
||||
Title string
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Org struct {
|
||||
Login string
|
||||
}
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Repo struct {
|
||||
Name string // owner/repo
|
||||
}
|
||||
Payload struct {
|
||||
Action string
|
||||
Issue IssueOrPR
|
||||
PullRequest IssueOrPR `json:"pull_request"`
|
||||
Comment struct {
|
||||
Body string
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Type string `json:"__typename"`
|
||||
UpdatedAt time.Time
|
||||
Title string
|
||||
Number int
|
||||
Repository struct {
|
||||
NameWithOwner string
|
||||
}
|
||||
}
|
||||
|
||||
type Results []SearchResult
|
||||
|
||||
func (rs Results) Len() int {
|
||||
return len(rs)
|
||||
}
|
||||
|
||||
func (rs Results) Less(i, j int) bool {
|
||||
return rs[i].UpdatedAt.After(rs[j].UpdatedAt)
|
||||
}
|
||||
|
||||
func (rs Results) Swap(i, j int) {
|
||||
rs[i], rs[j] = rs[j], rs[i]
|
||||
}
|
||||
|
||||
type StatusGetter struct {
|
||||
Client *http.Client
|
||||
cachedClient func(*http.Client, time.Duration) *http.Client
|
||||
Org string
|
||||
Exclude []string
|
||||
AssignedPRs []StatusItem
|
||||
AssignedIssues []StatusItem
|
||||
Mentions []StatusItem
|
||||
ReviewRequests []StatusItem
|
||||
RepoActivity []StatusItem
|
||||
}
|
||||
|
||||
func NewStatusGetter(client *http.Client, opts *StatusOptions) *StatusGetter {
|
||||
return &StatusGetter{
|
||||
Client: client,
|
||||
Org: opts.Org,
|
||||
Exclude: opts.Exclude,
|
||||
cachedClient: opts.CachedClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StatusGetter) CachedClient(ttl time.Duration) *http.Client {
|
||||
return s.cachedClient(s.Client, ttl)
|
||||
}
|
||||
|
||||
func (s *StatusGetter) ShouldExclude(repo string) bool {
|
||||
for _, exclude := range s.Exclude {
|
||||
if repo == exclude {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *StatusGetter) CurrentUsername() (string, error) {
|
||||
cachedClient := s.CachedClient(time.Hour * 48)
|
||||
cachingAPIClient := api.NewClientFromHTTP(cachedClient)
|
||||
currentUsername, err := api.CurrentLoginName(cachingAPIClient, ghinstance.Default())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get current username: %w", err)
|
||||
}
|
||||
|
||||
return currentUsername, nil
|
||||
}
|
||||
|
||||
func (s *StatusGetter) ActualMention(n Notification) (string, error) {
|
||||
currentUsername, err := s.CurrentUsername()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// long cache period since once a comment is looked up, it never needs to be
|
||||
// consulted again.
|
||||
cachedClient := s.CachedClient(time.Hour * 24 * 30)
|
||||
c := api.NewClientFromHTTP(cachedClient)
|
||||
resp := struct {
|
||||
Body string
|
||||
}{}
|
||||
if err := c.REST(ghinstance.Default(), "GET", n.Subject.LatestCommentURL, nil, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var ret string
|
||||
|
||||
if strings.Contains(resp.Body, "@"+currentUsername) {
|
||||
ret = resp.Body
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// These are split up by endpoint since it is along that boundary we parallelize
|
||||
// work
|
||||
|
||||
// Populate .Mentions
|
||||
func (s *StatusGetter) LoadNotifications() error {
|
||||
perPage := 100
|
||||
c := api.NewClientFromHTTP(s.Client)
|
||||
query := url.Values{}
|
||||
query.Add("per_page", fmt.Sprintf("%d", perPage))
|
||||
query.Add("participating", "true")
|
||||
query.Add("all", "true")
|
||||
|
||||
// this sucks, having to fetch so much :/ but it was the only way in my
|
||||
// testing to really get enough mentions. I would love to be able to just
|
||||
// filter for mentions but it does not seem like the notifications API can
|
||||
// do that. I'd switch to the GraphQL version, but to my knowledge that does
|
||||
// not work with PATs right now.
|
||||
var ns []Notification
|
||||
var resp []Notification
|
||||
pages := 0
|
||||
p := fmt.Sprintf("notifications?%s", query.Encode())
|
||||
for pages < 3 {
|
||||
next, err := c.RESTWithNext(ghinstance.Default(), "GET", p, nil, &resp)
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if !errors.As(err, &httpErr) || httpErr.StatusCode != 404 {
|
||||
return fmt.Errorf("could not get notifications: %w", err)
|
||||
}
|
||||
}
|
||||
ns = append(ns, resp...)
|
||||
|
||||
if next == "" || len(resp) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
pages++
|
||||
p = next
|
||||
}
|
||||
|
||||
s.Mentions = []StatusItem{}
|
||||
|
||||
for _, n := range ns {
|
||||
if n.Reason != "mention" {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.Org != "" && n.Repository.Owner.Login != s.Org {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.ShouldExclude(n.Repository.FullName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if actual, err := s.ActualMention(n); actual != "" && err == nil {
|
||||
// I'm so sorry
|
||||
split := strings.Split(n.Subject.URL, "/")
|
||||
s.Mentions = append(s.Mentions, StatusItem{
|
||||
Repository: n.Repository.FullName,
|
||||
Identifier: fmt.Sprintf("%s#%s", n.Repository.FullName, split[len(split)-1]),
|
||||
preview: actual,
|
||||
})
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("could not fetch comment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StatusGetter) buildSearchQuery() string {
|
||||
q := `
|
||||
query AssignedSearch {
|
||||
assignments: search(first: 25, type: ISSUE, query:"%s") {
|
||||
edges {
|
||||
node {
|
||||
...on Issue {
|
||||
__typename
|
||||
updatedAt
|
||||
title
|
||||
number
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
}
|
||||
...on PullRequest {
|
||||
updatedAt
|
||||
__typename
|
||||
title
|
||||
number
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reviewRequested: search(first: 25, type: ISSUE, query:"%s") {
|
||||
edges {
|
||||
node {
|
||||
...on PullRequest {
|
||||
updatedAt
|
||||
__typename
|
||||
title
|
||||
number
|
||||
repository {
|
||||
nameWithOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
assignmentsQ := `assignee:@me state:open%s%s`
|
||||
requestedQ := `state:open review-requested:@me%s%s`
|
||||
|
||||
orgFilter := ""
|
||||
if s.Org != "" {
|
||||
orgFilter = " org:" + s.Org
|
||||
}
|
||||
excludeFilter := ""
|
||||
for _, repo := range s.Exclude {
|
||||
excludeFilter += " -repo:" + repo
|
||||
}
|
||||
assignmentsQ = fmt.Sprintf(assignmentsQ, orgFilter, excludeFilter)
|
||||
requestedQ = fmt.Sprintf(requestedQ, orgFilter, excludeFilter)
|
||||
|
||||
return fmt.Sprintf(q, assignmentsQ, requestedQ)
|
||||
}
|
||||
|
||||
// Populate .AssignedPRs, .AssignedIssues, .ReviewRequests
|
||||
func (s *StatusGetter) LoadSearchResults() error {
|
||||
q := s.buildSearchQuery()
|
||||
c := api.NewClientFromHTTP(s.Client)
|
||||
|
||||
var resp struct {
|
||||
Assignments struct {
|
||||
Edges []struct {
|
||||
Node SearchResult
|
||||
}
|
||||
}
|
||||
ReviewRequested struct {
|
||||
Edges []struct {
|
||||
Node SearchResult
|
||||
}
|
||||
}
|
||||
}
|
||||
err := c.GraphQL(ghinstance.Default(), q, nil, &resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not search for assignments: %w", err)
|
||||
}
|
||||
|
||||
prs := []SearchResult{}
|
||||
issues := []SearchResult{}
|
||||
reviewRequested := []SearchResult{}
|
||||
|
||||
for _, e := range resp.Assignments.Edges {
|
||||
if e.Node.Type == "Issue" {
|
||||
issues = append(issues, e.Node)
|
||||
} else if e.Node.Type == "PullRequest" {
|
||||
prs = append(prs, e.Node)
|
||||
} else {
|
||||
panic("you shouldn't be here")
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range resp.ReviewRequested.Edges {
|
||||
reviewRequested = append(reviewRequested, e.Node)
|
||||
}
|
||||
|
||||
sort.Sort(Results(issues))
|
||||
sort.Sort(Results(prs))
|
||||
sort.Sort(Results(reviewRequested))
|
||||
|
||||
s.AssignedIssues = []StatusItem{}
|
||||
s.AssignedPRs = []StatusItem{}
|
||||
s.ReviewRequests = []StatusItem{}
|
||||
|
||||
for _, i := range issues {
|
||||
s.AssignedIssues = append(s.AssignedIssues, StatusItem{
|
||||
Repository: i.Repository.NameWithOwner,
|
||||
Identifier: fmt.Sprintf("%s#%d", i.Repository.NameWithOwner, i.Number),
|
||||
preview: i.Title,
|
||||
})
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
s.AssignedPRs = append(s.AssignedPRs, StatusItem{
|
||||
Repository: pr.Repository.NameWithOwner,
|
||||
Identifier: fmt.Sprintf("%s#%d", pr.Repository.NameWithOwner, pr.Number),
|
||||
preview: pr.Title,
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range reviewRequested {
|
||||
s.ReviewRequests = append(s.ReviewRequests, StatusItem{
|
||||
Repository: r.Repository.NameWithOwner,
|
||||
Identifier: fmt.Sprintf("%s#%d", r.Repository.NameWithOwner, r.Number),
|
||||
preview: r.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Populate .RepoActivity
|
||||
func (s *StatusGetter) LoadEvents() error {
|
||||
perPage := 100
|
||||
c := api.NewClientFromHTTP(s.Client)
|
||||
query := url.Values{}
|
||||
query.Add("per_page", fmt.Sprintf("%d", perPage))
|
||||
|
||||
currentUsername, err := s.CurrentUsername()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var events []Event
|
||||
var resp []Event
|
||||
pages := 0
|
||||
p := fmt.Sprintf("users/%s/received_events?%s", currentUsername, query.Encode())
|
||||
for pages < 2 {
|
||||
next, err := c.RESTWithNext(ghinstance.Default(), "GET", p, nil, &resp)
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if !errors.As(err, &httpErr) || httpErr.StatusCode != 404 {
|
||||
return fmt.Errorf("could not get events: %w", err)
|
||||
}
|
||||
}
|
||||
events = append(events, resp...)
|
||||
if next == "" || len(resp) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
pages++
|
||||
p = next
|
||||
}
|
||||
|
||||
s.RepoActivity = []StatusItem{}
|
||||
|
||||
for _, e := range events {
|
||||
if s.Org != "" && e.Org.Login != s.Org {
|
||||
continue
|
||||
}
|
||||
if s.ShouldExclude(e.Repo.Name) {
|
||||
continue
|
||||
}
|
||||
si := StatusItem{}
|
||||
var number int
|
||||
switch e.Type {
|
||||
case "IssuesEvent":
|
||||
if e.Payload.Action != "opened" {
|
||||
continue
|
||||
}
|
||||
si.Reason = "new issue"
|
||||
si.preview = e.Payload.Issue.Title
|
||||
number = e.Payload.Issue.Number
|
||||
case "PullRequestEvent":
|
||||
if e.Payload.Action != "opened" {
|
||||
continue
|
||||
}
|
||||
si.Reason = "new PR"
|
||||
si.preview = e.Payload.PullRequest.Title
|
||||
number = e.Payload.PullRequest.Number
|
||||
case "PullRequestReviewCommentEvent":
|
||||
si.Reason = "comment on " + e.Payload.PullRequest.Title
|
||||
si.preview = e.Payload.Comment.Body
|
||||
number = e.Payload.PullRequest.Number
|
||||
case "IssueCommentEvent":
|
||||
si.Reason = "comment on " + e.Payload.Issue.Title
|
||||
si.preview = e.Payload.Comment.Body
|
||||
number = e.Payload.Issue.Number
|
||||
default:
|
||||
continue
|
||||
}
|
||||
si.Repository = e.Repo.Name
|
||||
si.Identifier = fmt.Sprintf("%s#%d", e.Repo.Name, number)
|
||||
s.RepoActivity = append(s.RepoActivity, si)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func statusRun(opts *StatusOptions) error {
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create client: %w", err)
|
||||
}
|
||||
|
||||
sg := NewStatusGetter(client, opts)
|
||||
|
||||
// TODO break out sections into individual subcommands
|
||||
|
||||
g := new(errgroup.Group)
|
||||
opts.IO.StartProgressIndicator()
|
||||
g.Go(func() error {
|
||||
err := sg.LoadNotifications()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not load notifications: %w", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
err := sg.LoadEvents()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not load events: %w", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
err := sg.LoadSearchResults()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to search: %w", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
err = g.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
out := opts.IO.Out
|
||||
fullWidth := opts.IO.TerminalWidth()
|
||||
halfWidth := (fullWidth / 2) - 2
|
||||
|
||||
idStyle := cs.Cyan
|
||||
leftHalfStyle := lipgloss.NewStyle().Width(halfWidth).Padding(0).MarginRight(1).BorderRight(true).BorderStyle(lipgloss.NormalBorder())
|
||||
rightHalfStyle := lipgloss.NewStyle().Width(halfWidth).Padding(0)
|
||||
|
||||
section := func(header string, items []StatusItem, width, rowLimit int) (string, error) {
|
||||
tableOut := &bytes.Buffer{}
|
||||
fmt.Fprintln(tableOut, cs.Bold(header))
|
||||
tp := utils.NewTablePrinterWithOptions(opts.IO, utils.TablePrinterOptions{
|
||||
IsTTY: opts.IO.IsStdoutTTY(),
|
||||
MaxWidth: width,
|
||||
Out: tableOut,
|
||||
})
|
||||
if len(items) == 0 {
|
||||
tp.AddField("Nothing here ^_^", nil, nil)
|
||||
tp.EndRow()
|
||||
} else {
|
||||
for i, si := range items {
|
||||
if i == rowLimit {
|
||||
break
|
||||
}
|
||||
tp.AddField(si.Identifier, nil, idStyle)
|
||||
if si.Reason != "" {
|
||||
tp.AddField(si.Reason, nil, nil)
|
||||
}
|
||||
tp.AddField(si.Preview(), nil, nil)
|
||||
tp.EndRow()
|
||||
}
|
||||
}
|
||||
|
||||
err := tp.Render()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tableOut.String(), nil
|
||||
}
|
||||
|
||||
mSection, err := section("Mentions", sg.Mentions, halfWidth, 5)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render 'Mentions': %w", err)
|
||||
}
|
||||
mSection = rightHalfStyle.Render(mSection)
|
||||
|
||||
rrSection, err := section("Review Requests", sg.ReviewRequests, halfWidth, 5)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render 'Review Requests': %w", err)
|
||||
}
|
||||
rrSection = leftHalfStyle.Render(rrSection)
|
||||
|
||||
prSection, err := section("Assigned Pull Requests", sg.AssignedPRs, halfWidth, 5)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render 'Assigned Pull Requests': %w", err)
|
||||
}
|
||||
prSection = rightHalfStyle.Render(prSection)
|
||||
|
||||
issueSection, err := section("Assigned Issues", sg.AssignedIssues, halfWidth, 5)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render 'Assigned Issues': %w", err)
|
||||
}
|
||||
issueSection = leftHalfStyle.Render(issueSection)
|
||||
|
||||
raSection, err := section("Repository Activity", sg.RepoActivity, fullWidth, 10)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render 'Repository Activity': %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, lipgloss.JoinHorizontal(lipgloss.Top, issueSection, prSection))
|
||||
fmt.Fprintln(out, lipgloss.JoinHorizontal(lipgloss.Top, rrSection, mSection))
|
||||
fmt.Fprintln(out, raSection)
|
||||
|
||||
return nil
|
||||
}
|
||||
336
pkg/cmd/status/status_test.go
Normal file
336
pkg/cmd/status/status_test.go
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants StatusOptions
|
||||
}{
|
||||
{
|
||||
name: "defaults",
|
||||
},
|
||||
{
|
||||
name: "org",
|
||||
cli: "-o cli",
|
||||
wants: StatusOptions{
|
||||
Org: "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exclude",
|
||||
cli: "-e cli/cli,cli/go-gh",
|
||||
wants: StatusOptions{
|
||||
Exclude: []string{"cli/cli", "cli/go-gh"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStdoutTTY(true)
|
||||
|
||||
if tt.wants.Exclude == nil {
|
||||
tt.wants.Exclude = []string{}
|
||||
}
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *StatusOptions
|
||||
cmd := NewCmdStatus(f, func(opts *StatusOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
_, err = cmd.ExecuteC()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.Org, gotOpts.Org)
|
||||
assert.Equal(t, tt.wants.Exclude, gotOpts.Exclude)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
opts *StatusOptions
|
||||
wantOut string
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nothing",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("AssignedSearch"),
|
||||
httpmock.StringResponse(`{"data": { "assignments": {"edges": [] }, "reviewRequested": {"edges": []}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "notifications"),
|
||||
httpmock.StringResponse(`[]`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/jillvalentine/received_events"),
|
||||
httpmock.StringResponse(`[]`))
|
||||
},
|
||||
opts: &StatusOptions{},
|
||||
wantOut: "Assigned Issues │ Assigned Pull Requests \nNothing here ^_^ │ Nothing here ^_^ \n │ \nReview Requests │ Mentions \nNothing here ^_^ │ Nothing here ^_^ \n │ \nRepository Activity\nNothing here ^_^\n\n",
|
||||
},
|
||||
{
|
||||
name: "something",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
|
||||
httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
|
||||
httpmock.StringResponse(`{"body":"this is a comment"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/cli/cli/issues/1096"),
|
||||
httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
|
||||
httpmock.StringResponse(`{"body":"not a real mention"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
|
||||
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("AssignedSearch"),
|
||||
httpmock.FileResponse("./fixtures/search.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "notifications"),
|
||||
httpmock.FileResponse("./fixtures/notifications.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/jillvalentine/received_events"),
|
||||
httpmock.FileResponse("./fixtures/events.json"))
|
||||
},
|
||||
opts: &StatusOptions{},
|
||||
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ rpd/todo#110 hello @j...\nvilmibm/testing#1234 Foobar │ cli/cli#1096 @jillval...\nrpd/todo#50 Welcome party...│ vilmibm/gh-screensaver#15 a messag...\ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on W...\nvilmibm/testing#5325 comment on Ability to sea... We are working on dedicat...\ncli/cli#5319 comment on [Codespaces] D... Wondering if we shouldn't...\ncli/cli#5300 new issue Terminal bell when a runn...\n\n",
|
||||
},
|
||||
{
|
||||
name: "exclude a repository",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
|
||||
httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
|
||||
httpmock.StringResponse(`{"body":"this is a comment"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
|
||||
httpmock.StringResponse(`{"body":"not a real mention"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
|
||||
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("AssignedSearch"),
|
||||
httpmock.FileResponse("./fixtures/search.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "notifications"),
|
||||
httpmock.FileResponse("./fixtures/notifications.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/jillvalentine/received_events"),
|
||||
httpmock.FileResponse("./fixtures/events.json"))
|
||||
},
|
||||
opts: &StatusOptions{
|
||||
Exclude: []string{"cli/cli"},
|
||||
},
|
||||
// NOTA BENE: you'll see cli/cli in search results because that happens
|
||||
// server side and the fixture doesn't account for that
|
||||
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ rpd/todo#110 hello @j...\nvilmibm/testing#1234 Foobar │ vilmibm/gh-screensaver#15 a messag...\nrpd/todo#50 Welcome party...│ \ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on W...\nvilmibm/testing#5325 comment on Ability to sea... We are working on dedicat...\n\n",
|
||||
},
|
||||
{
|
||||
name: "exclude repositories",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
|
||||
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("AssignedSearch"),
|
||||
httpmock.FileResponse("./fixtures/search.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "notifications"),
|
||||
httpmock.FileResponse("./fixtures/notifications.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/jillvalentine/received_events"),
|
||||
httpmock.FileResponse("./fixtures/events.json"))
|
||||
},
|
||||
opts: &StatusOptions{
|
||||
Exclude: []string{"cli/cli", "rpd/todo"},
|
||||
},
|
||||
// NOTA BENE: you'll see cli/cli in search results because that happens
|
||||
// server side and the fixture doesn't account for that
|
||||
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ vilmibm/gh-screensaver#15 a messag...\nvilmibm/testing#1234 Foobar │ \nrpd/todo#50 Welcome party...│ \ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nvilmibm/testing#5325 comment on Ability to sea... We are working on dedicat...\n\n",
|
||||
},
|
||||
{
|
||||
name: "filter to an org",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
|
||||
httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
|
||||
httpmock.StringResponse(`{"body":"this is a comment"}`))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
|
||||
httpmock.StringResponse(`{"body":"not a real mention"}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("UserCurrent"),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL("AssignedSearch"),
|
||||
httpmock.FileResponse("./fixtures/search.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "notifications"),
|
||||
httpmock.FileResponse("./fixtures/notifications.json"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "users/jillvalentine/received_events"),
|
||||
httpmock.FileResponse("./fixtures/events.json"))
|
||||
},
|
||||
opts: &StatusOptions{
|
||||
Org: "rpd",
|
||||
},
|
||||
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ rpd/todo#110 hello @jillvalentine ...\nvilmibm/testing#1234 Foobar │ \nrpd/todo#50 Welcome party...│ \ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on Windows where it is needed\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
tt.opts.CachedClient = func(c *http.Client, _ time.Duration) *http.Client {
|
||||
return c
|
||||
}
|
||||
io, _, stdout, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
tt.opts.IO = io
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := statusRun(tt.opts)
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.Equal(t, tt.wantErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_buildSearchQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sg StatusGetter
|
||||
wantReviewQ string
|
||||
wantAssignedQ string
|
||||
}{
|
||||
{
|
||||
name: "nothing",
|
||||
wantReviewQ: "first: 25, type: ISSUE, query:\"state:open review-requested:@me",
|
||||
wantAssignedQ: "assignee:@me state:open",
|
||||
},
|
||||
{
|
||||
name: "exclude one",
|
||||
sg: StatusGetter{
|
||||
Exclude: []string{"cli/cli"},
|
||||
},
|
||||
wantReviewQ: "first: 25, type: ISSUE, query:\"state:open review-requested:@me -repo:cli/cli",
|
||||
wantAssignedQ: "assignee:@me state:open",
|
||||
},
|
||||
{
|
||||
name: "exclude several",
|
||||
sg: StatusGetter{
|
||||
Exclude: []string{"cli/cli", "vilmibm/testing"},
|
||||
},
|
||||
wantReviewQ: "first: 25, type: ISSUE, query:\"state:open review-requested:@me -repo:cli/cli -repo:vilmibm/testing",
|
||||
wantAssignedQ: "assignee:@me state:open",
|
||||
},
|
||||
{
|
||||
name: "org filter",
|
||||
sg: StatusGetter{
|
||||
Org: "cli",
|
||||
},
|
||||
wantReviewQ: "first: 25, type: ISSUE, query:\"state:open review-requested:@me org:cli",
|
||||
wantAssignedQ: "assignee:@me state:open org:cli",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Contains(t, tt.sg.buildSearchQuery(), tt.wantReviewQ)
|
||||
assert.Contains(t, tt.sg.buildSearchQuery(), tt.wantAssignedQ)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,10 +30,7 @@ func REST(method, p string) Matcher {
|
|||
if !strings.EqualFold(req.Method, method) {
|
||||
return false
|
||||
}
|
||||
if req.URL.Path != "/"+p {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return req.URL.Path == "/"+p
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ type TablePrinter interface {
|
|||
}
|
||||
|
||||
type TablePrinterOptions struct {
|
||||
IsTTY bool
|
||||
IsTTY bool
|
||||
MaxWidth int
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
func NewTablePrinter(io *iostreams.IOStreams) TablePrinter {
|
||||
|
|
@ -27,21 +29,29 @@ func NewTablePrinter(io *iostreams.IOStreams) TablePrinter {
|
|||
})
|
||||
}
|
||||
|
||||
func NewTablePrinterWithOptions(io *iostreams.IOStreams, opts TablePrinterOptions) TablePrinter {
|
||||
func NewTablePrinterWithOptions(ios *iostreams.IOStreams, opts TablePrinterOptions) TablePrinter {
|
||||
var out io.Writer
|
||||
if opts.Out != nil {
|
||||
out = opts.Out
|
||||
} else {
|
||||
out = ios.Out
|
||||
}
|
||||
if opts.IsTTY {
|
||||
var maxWidth int
|
||||
if io.IsStdoutTTY() {
|
||||
maxWidth = io.TerminalWidth()
|
||||
if opts.MaxWidth > 0 {
|
||||
maxWidth = opts.MaxWidth
|
||||
} else if ios.IsStdoutTTY() {
|
||||
maxWidth = ios.TerminalWidth()
|
||||
} else {
|
||||
maxWidth = io.ProcessTerminalWidth()
|
||||
maxWidth = ios.ProcessTerminalWidth()
|
||||
}
|
||||
return &ttyTablePrinter{
|
||||
out: io.Out,
|
||||
out: out,
|
||||
maxWidth: maxWidth,
|
||||
}
|
||||
}
|
||||
return &tsvTablePrinter{
|
||||
out: io.Out,
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue