Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add module GithubAllRepos + fix linter errors #1706

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/widget_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/wtfutil/wtf/modules/gerrit"
"github.com/wtfutil/wtf/modules/git"
"github.com/wtfutil/wtf/modules/github"
"github.com/wtfutil/wtf/modules/githuballrepos"
"github.com/wtfutil/wtf/modules/gitlab"
"github.com/wtfutil/wtf/modules/gitlabtodo"
"github.com/wtfutil/wtf/modules/gitter"
Expand Down Expand Up @@ -198,6 +199,9 @@ func MakeWidget(
case "github":
settings := github.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = github.NewWidget(tviewApp, redrawChan, pages, settings)
case "githuballrepos":
settings := githuballrepos.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = githuballrepos.NewWidget(tviewApp, redrawChan, pages, settings)
case "gitlab":
settings := gitlab.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = gitlab.NewWidget(tviewApp, redrawChan, pages, settings)
Expand Down
2 changes: 1 addition & 1 deletion modules/buildkite/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (widget *Widget) recentBuilds(pipeline PipelineSettings) ([]Build, error) {
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf(resp.Status)
return nil, fmt.Errorf("resp status code is not 2**. Status code: %v", resp.Status)
}

builds := []Build{}
Expand Down
2 changes: 1 addition & 1 deletion modules/circleci/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (client *Client) circleRequest(path string) ([]byte, error) {
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf(resp.Status)
return nil, fmt.Errorf("resp status code is not 2**. Status code: %v", resp.Status)
}

body, err := io.ReadAll(resp.Body)
Expand Down
4 changes: 2 additions & 2 deletions modules/covid/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func LatestCases() (*Cases, error) {
latestURL := covidTrackerAPIURL + "latest"
resp, err := http.Get(latestURL)
if resp.StatusCode != 200 {
return nil, fmt.Errorf(resp.Status)
return nil, fmt.Errorf("resp status code is not 2**. Status code: %v", resp.Status)
}
if err != nil {
return nil, err
Expand All @@ -37,7 +37,7 @@ func (widget *Widget) LatestCountryCases(countries []interface{}) ([]*Cases, err
countryURL := covidTrackerAPIURL + "locations?source=jhu&country_code=" + name.(string)
resp, err := http.Get(countryURL)
if resp.StatusCode != 200 {
return nil, fmt.Errorf(resp.Status)
return nil, fmt.Errorf("resp status code is not 2**. Status code: %v", resp.Status)
}
if err != nil {
return nil, err
Expand Down
107 changes: 107 additions & 0 deletions modules/githuballrepos/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package githuballrepos

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"

"io/fs"

"github.com/wtfutil/wtf/logger"
)

type Cacher interface {
Get() *WidgetData
Set(data *WidgetData)
IsValid() bool
}

const cacheDuration = 5 * time.Minute

// Cache stores the widget data and its expiration time
type Cache struct {
data *WidgetData
expires time.Time
configPath string
}

// NewCache creates a new Cache instance
func NewCache(configPath string) *Cache {
cache := &Cache{
configPath: configPath,
}

// Ensure the cache directory exists
cacheDir := filepath.Dir(cache.configPath)
logger.Log(fmt.Sprintf("Cache directory: %s\n", cacheDir))
if err := os.MkdirAll(cacheDir, 0600); err != nil {
logger.Log(fmt.Sprintf("Error creating cache directory: %s\n", err))
}

cache.load()
return cache
}

// Set updates the cache with new data
func (c *Cache) Set(data *WidgetData) {
c.data = data
c.expires = time.Now().Add(cacheDuration)
c.save()
}

// Get retrieves the cached data
func (c *Cache) Get() *WidgetData {
return c.data
}

// IsValid checks if the cache is still valid
func (c *Cache) IsValid() bool {
return c.data != nil && time.Now().Before(c.expires)
}

// save writes the cache data to disk
func (c *Cache) save() {
cacheData := struct {
Data *WidgetData `json:"data"`
Expires time.Time `json:"expires"`
}{
Data: c.data,
Expires: c.expires,
}

jsonData, err := json.Marshal(cacheData)
if err != nil {
logger.Log(fmt.Sprintf("Error marshaling cache data: %s\n", err))
return
}

if err := os.WriteFile(c.configPath, jsonData, fs.FileMode(0644)); err != nil {
logger.Log(fmt.Sprintf("Error writing cache file: %s\n", err))
}
}

// load reads the cache data from disk
func (c *Cache) load() {
jsonData, err := os.ReadFile(c.configPath)
if err != nil {
if !os.IsNotExist(err) {
logger.Log(fmt.Sprintf("Error reading cache file: %s\n", err))
}
return
}

var cacheData struct {
Data *WidgetData `json:"data"`
Expires time.Time `json:"expires"`
}

if err := json.Unmarshal(jsonData, &cacheData); err != nil {
logger.Log(fmt.Sprintf("Error unmarshaling cache data: %s\n", err))
return
}

c.data = cacheData.Data
c.expires = cacheData.Expires
}
146 changes: 146 additions & 0 deletions modules/githuballrepos/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package githuballrepos

import (
"context"
"fmt"

"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)

type GitHubFetcher interface {
FetchData(orgs []string, username string) *WidgetData
}

// GitHubClient handles communication with the GitHub API
type GitHubClient struct {
client *githubv4.Client
}

// NewGitHubClient creates a new GitHubClient
func NewGitHubClient(token string) *GitHubClient {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
httpClient := oauth2.NewClient(context.Background(), src)

return &GitHubClient{
client: githubv4.NewClient(httpClient),
}
}

// FetchData retrieves all required data from GitHub
func (c *GitHubClient) FetchData(orgs []string, username string) *WidgetData {
data := &WidgetData{
MyPRs: make([]PR, 0),
PRReviewRequests: make([]PR, 0),
WatchedPRs: make([]PR, 0),
}

for _, org := range orgs {
data.PRsOpenedByMe += c.fetchPRCount(org, username, "author")
data.PRReviewRequestsCount += c.fetchPRCount(org, username, "review-requested")
data.OpenIssuesCount += c.fetchIssueCount(org)

data.MyPRs = append(data.MyPRs, c.fetchPRs(org, username, "author")...)
data.PRReviewRequests = append(data.PRReviewRequests, c.fetchPRs(org, username, "review-requested")...)
data.WatchedPRs = append(data.WatchedPRs, c.fetchPRs(org, username, "involves")...)
}

return data
}

func (c *GitHubClient) fetchPRCount(org, username, filter string) int {
var query struct {
Search struct {
IssueCount int
} `graphql:"search(query: $query, type: ISSUE, first: 0)"`
}

variables := map[string]interface{}{
"query": githubv4.String(fmt.Sprintf("org:%s is:pr is:open %s:%s", org, filter, username)),
}

err := c.client.Query(context.Background(), &query, variables)
if err != nil {
// Handle error (log it, etc.)
return 0
}

return query.Search.IssueCount
}

func (c *GitHubClient) fetchIssueCount(org string) int {
var query struct {
Search struct {
IssueCount int
} `graphql:"search(query: $query, type: ISSUE, first: 0)"`
}

variables := map[string]interface{}{
"query": githubv4.String(fmt.Sprintf("org:%s is:issue is:open", org)),
}

err := c.client.Query(context.Background(), &query, variables)
if err != nil {
// Handle error (log it, etc.)
return 0
}

return query.Search.IssueCount
}

func (c *GitHubClient) fetchPRs(org, username, filter string) []PR {
var query struct {
Search struct {
Nodes []struct {
PullRequest struct {
Title string
URL string
Author struct {
Login string
}
Repository struct {
Name string
}
} `graphql:"... on PullRequest"`
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"search(query: $query, type: ISSUE, first: 100, after: $cursor)"`
}

variables := map[string]interface{}{
"query": githubv4.String(fmt.Sprintf("org:%s is:pr is:open %s:%s", org, filter, username)),
"cursor": (*githubv4.String)(nil), // Null for first request
}

var allPRs []PR

for {
err := c.client.Query(context.Background(), &query, variables)
if err != nil {
// Handle error (log it, etc.)
break
}

for _, node := range query.Search.Nodes {
pr := node.PullRequest
allPRs = append(allPRs, PR{
Title: pr.Title,
URL: pr.URL,
Author: pr.Author.Login,
Repository: pr.Repository.Name,
})
}

if !query.Search.PageInfo.HasNextPage {
break
}
variables["cursor"] = githubv4.NewString(query.Search.PageInfo.EndCursor)
}

return allPRs
}
57 changes: 57 additions & 0 deletions modules/githuballrepos/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package githuballrepos

import (
"fmt"
"strings"
)

// WidgetData holds all the data for the widget
type WidgetData struct {
PRsOpenedByMe int
PRReviewRequestsCount int
OpenIssuesCount int

MyPRs []PR
PRReviewRequests []PR
WatchedPRs []PR
}

// PR represents a single pull request
type PR struct {
Title string
URL string
Author string
Repository string
}

// FormatCounters returns a formatted string of counters
func (d *WidgetData) FormatCounters() string {
return fmt.Sprintf(
"PRs opened by me: %d\nPR review requests: %d\nOpen issues: %d\n",
d.PRsOpenedByMe,
d.PRReviewRequestsCount,
d.OpenIssuesCount,
)
}

// FormatPRs returns a formatted string of PRs
func (d *WidgetData) FormatPRs() string {
var sb strings.Builder

sb.WriteString("[green]My PRs:[white]\n")
for _, pr := range d.MyPRs {
sb.WriteString(fmt.Sprintf("- %s (%s)\n", pr.Title, pr.Repository))
}

sb.WriteString("\n[yellow]PR Review Requests:[white]\n")
for _, pr := range d.PRReviewRequests {
sb.WriteString(fmt.Sprintf("- %s (%s)\n", pr.Title, pr.Repository))
}

sb.WriteString("\n[blue]Watched PRs:[white]\n")
for _, pr := range d.WatchedPRs {
sb.WriteString(fmt.Sprintf("- %s (%s by %s)\n", pr.Title, pr.Repository, pr.Author))
}

return sb.String()
}
Loading
Loading