-
-
Notifications
You must be signed in to change notification settings - Fork 812
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
868 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, 0755); 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
Oops, something went wrong.