diff --git a/data/issueapi.go b/data/issueapi.go index a5c8c2c3..b1c385a2 100644 --- a/data/issueapi.go +++ b/data/issueapi.go @@ -39,6 +39,10 @@ type IssueLabels struct { Nodes []Label } +func (data IssueData) GetTitle() string { + return data.Title +} + func (data IssueData) GetRepoNameWithOwner() string { return data.Repository.NameWithOwner } diff --git a/data/prapi.go b/data/prapi.go index 712205d2..bdf40903 100644 --- a/data/prapi.go +++ b/data/prapi.go @@ -2,11 +2,13 @@ package data import ( "fmt" + "net/url" "time" "github.com/charmbracelet/log" gh "github.com/cli/go-gh/v2/pkg/api" graphql "github.com/cli/shurcooL-graphql" + "github.com/shurcooL/githubv4" ) type PullRequestData struct { @@ -118,6 +120,10 @@ type PageInfo struct { EndCursor string } +func (data PullRequestData) GetTitle() string { + return data.Title +} + func (data PullRequestData) GetRepoNameWithOwner() string { return data.Repository.NameWithOwner } @@ -191,3 +197,33 @@ func FetchPullRequests(query string, limit int, pageInfo *PageInfo) (PullRequest PageInfo: queryResult.Search.PageInfo, }, nil } + +func FetchPullRequest(prUrl string) (PullRequestData, error) { + var err error + client, err := gh.DefaultGraphQLClient() + + if err != nil { + return PullRequestData{}, err + } + + var queryResult struct { + Resource struct { + PullRequest PullRequestData `graphql:"... on PullRequest"` + } `graphql:"resource(url: $url)"` + } + parsedUrl, err := url.Parse(prUrl) + if err != nil { + return PullRequestData{}, err + } + variables := map[string]interface{}{ + "url": githubv4.URI{URL: parsedUrl}, + } + log.Debug("Fetching PR", "url", prUrl) + err = client.Query("FetchPullRequest", &queryResult, variables) + if err != nil { + return PullRequestData{}, err + } + log.Debug("Successfully fetched PR", "url", prUrl, "data", queryResult.Resource.PullRequest) + + return queryResult.Resource.PullRequest, nil +} diff --git a/data/utils.go b/data/utils.go index 19309a97..bad7ff58 100644 --- a/data/utils.go +++ b/data/utils.go @@ -1,9 +1,12 @@ package data -import "time" +import ( + "time" +) type RowData interface { GetRepoNameWithOwner() string + GetTitle() string GetNumber() int GetUrl() string GetUpdatedAt() time.Time diff --git a/go.mod b/go.mod index 32d7308d..431c41fd 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,12 @@ require ( github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/henvic/httpretty v0.1.3 // indirect @@ -45,9 +48,13 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect + github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/shurcooL/githubv4 v0.0.0-20231126234147-1cffa1f02456 // indirect + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect github.com/yuin/goldmark v1.6.0 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect diff --git a/go.sum b/go.sum index 3b155ada..508b5ee4 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc h1:NNgdMgPX3j33uEAoVVxNxillDPnxT0xbGv8uh4CKIAo= +github.com/gen2brain/beeep v0.0.0-20230907135156-1a38885a97fc/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -69,6 +71,10 @@ github.com/go-playground/validator/v10 v10.15.0 h1:nDU5XeOKtB3GEa+uB7GNYwhVKsgjA github.com/go-playground/validator/v10 v10.15.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= +github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -117,6 +123,8 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -126,6 +134,10 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/githubv4 v0.0.0-20231126234147-1cffa1f02456 h1:6dExqsYngGEiixqa1vmtlUd+zbyISilg0Cf3GWVdeYM= +github.com/shurcooL/githubv4 v0.0.0-20231126234147-1cffa1f02456/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -140,6 +152,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= +github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/ui/components/prssection/prssection.go b/ui/components/prssection/prssection.go index 151d7d05..1ed52262 100644 --- a/ui/components/prssection/prssection.go +++ b/ui/components/prssection/prssection.go @@ -117,6 +117,10 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) { if err != nil { m.Ctx.Error = err } + + case key.Matches(msg, keys.PRKeys.WatchChecks): + cmd = m.watchChecks() + } case UpdatePRMsg: diff --git a/ui/components/prssection/watchChecks.go b/ui/components/prssection/watchChecks.go new file mode 100644 index 00000000..87931ebe --- /dev/null +++ b/ui/components/prssection/watchChecks.go @@ -0,0 +1,91 @@ +package prssection + +import ( + "bytes" + "fmt" + "os/exec" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/log" + "github.com/gen2brain/beeep" + + "github.com/dlvhdr/gh-dash/data" + prComponent "github.com/dlvhdr/gh-dash/ui/components/pr" + "github.com/dlvhdr/gh-dash/ui/constants" + "github.com/dlvhdr/gh-dash/ui/context" +) + +func (m *Model) watchChecks() tea.Cmd { + pr := m.GetCurrRow() + prNumber := pr.GetNumber() + title := pr.GetTitle() + url := pr.GetUrl() + repoNameWithOwner := pr.GetRepoNameWithOwner() + taskId := fmt.Sprintf("pr_reopen_%d", prNumber) + task := context.Task{ + Id: taskId, + StartText: fmt.Sprintf("Watching checks for PR #%d", prNumber), + FinishedText: fmt.Sprintf("Watching checks for PR #%d", prNumber), + State: context.TaskStart, + Error: nil, + } + startCmd := m.Ctx.StartTask(task) + return tea.Batch(startCmd, func() tea.Msg { + c := exec.Command( + "gh", + "pr", + "checks", + "--watch", + "--fail-fast", + fmt.Sprint(m.GetCurrRow().GetNumber()), + "-R", + m.GetCurrRow().GetRepoNameWithOwner(), + ) + + var outb, errb bytes.Buffer + c.Stdout = &outb + c.Stderr = &errb + + err := c.Start() + go func() { + err := c.Wait() + if err != nil { + log.Error("Error waiting for watch command to finish", "err", err) + } + + // TODO: check for installation of terminal-notifier or alternative as logo isn't supported + updatedPr, err := data.FetchPullRequest(url) + if err != nil { + log.Error("Error fetching updated PR details", "url", url, "err", err) + } + + renderedPr := prComponent.PullRequest{Ctx: m.Ctx, Data: updatedPr} + checksRollup := " Checks are pending" + switch renderedPr.GetStatusChecksRollup() { + case "SUCCESS": + checksRollup = "✅ Checks have passed" + case "FAILURE": + checksRollup = "❌ Checks have failed" + } + + err = beeep.Notify( + fmt.Sprintf("gh-dash: %s", title), + fmt.Sprintf("PR #%d in %s\n%s", prNumber, repoNameWithOwner, checksRollup), + "", + ) + if err != nil { + log.Error("Error showing system notification", "err", err) + } + }() + + return constants.TaskFinishedMsg{ + SectionId: m.Id, + SectionType: SectionType, + TaskId: taskId, + Err: err, + Msg: UpdatePRMsg{ + PrNumber: prNumber, + }, + } + }) +} diff --git a/ui/keys/prKeys.go b/ui/keys/prKeys.go index ac41368f..55650647 100644 --- a/ui/keys/prKeys.go +++ b/ui/keys/prKeys.go @@ -1,17 +1,20 @@ package keys -import "github.com/charmbracelet/bubbles/key" +import ( + "github.com/charmbracelet/bubbles/key" +) type PRKeyMap struct { - Assign key.Binding - Unassign key.Binding - Comment key.Binding - Diff key.Binding - Checkout key.Binding - Close key.Binding - Ready key.Binding - Reopen key.Binding - Merge key.Binding + Assign key.Binding + Unassign key.Binding + Comment key.Binding + Diff key.Binding + Checkout key.Binding + Close key.Binding + Ready key.Binding + Reopen key.Binding + Merge key.Binding + WatchChecks key.Binding } var PRKeys = PRKeyMap{ @@ -44,13 +47,17 @@ var PRKeys = PRKeyMap{ key.WithHelp("X", "reopen"), ), Ready: key.NewBinding( - key.WithKeys("w"), - key.WithHelp("w", "ready for review"), + key.WithKeys("W"), + key.WithHelp("W", "ready for review"), ), Merge: key.NewBinding( key.WithKeys("m"), key.WithHelp("m", "merge"), ), + WatchChecks: key.NewBinding( + key.WithKeys("w"), + key.WithHelp("w", "Watch checks"), + ), } func PRFullHelp() []key.Binding { @@ -64,5 +71,6 @@ func PRFullHelp() []key.Binding { PRKeys.Ready, PRKeys.Reopen, PRKeys.Merge, + PRKeys.WatchChecks, } } diff --git a/ui/ui.go b/ui/ui.go index 05b76d45..487ba388 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -249,7 +249,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.sidebar.ScrollToBottom() return m, cmd - case key.Matches(msg, keys.PRKeys.Close, keys.PRKeys.Reopen, keys.PRKeys.Ready, keys.PRKeys.Merge, keys.IssueKeys.Close, keys.IssueKeys.Reopen): + case key.Matches( + msg, + keys.PRKeys.Close, + keys.PRKeys.Reopen, + keys.PRKeys.Ready, + keys.PRKeys.Merge, + keys.IssueKeys.Close, + keys.IssueKeys.Reopen, + ): var action string switch {