Last active
December 26, 2023 07:16
-
-
Save BusyJay/cfd0bfd89d87e03f72bcd70284c50d4a to your computer and use it in GitHub Desktop.
An easy script to count your work on Github.
This file contains hidden or 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
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"os" | |
"os/exec" | |
"runtime" | |
"strings" | |
"time" | |
) | |
const outputFile = "output.html" | |
var githubToken = "" | |
var offset = flag.Int("offset", 0, "offset in days from now") | |
var githubTokenFile = flag.String("token-path", "", "path to file that contains token") | |
type PullRequest struct { | |
Title string `json:"title"` | |
Url string `json:"url"` | |
} | |
type PullRequestNode struct { | |
PullRequest PullRequest `json:"pullRequest"` | |
OccurredAt time.Time `json:"occurredAt"` | |
} | |
type PullRequestReviewNode struct { | |
PullRequest PullRequest `json:"pullRequest"` | |
OccurredAt time.Time `json:"occurredAt"` | |
} | |
type PullRequestReviewEdge struct { | |
Node PullRequestReviewNode `json:"node"` | |
} | |
type PullRequestEdge struct { | |
Node PullRequestNode `json:"node"` | |
} | |
type ReviewContributions struct { | |
Edges []PullRequestReviewEdge `json:"edges"` | |
} | |
type PullRequestContributions struct { | |
Edges []PullRequestEdge `json:"edges"` | |
} | |
type Issue struct { | |
Title string `json:"title"` | |
Url string `json:"url"` | |
} | |
type IssueNode struct { | |
Issue Issue `json:"issue"` | |
OccurredAt time.Time `json:"occurredAt"` | |
} | |
type IssueEdge struct { | |
Node IssueNode `json:"node"` | |
} | |
type IssueCreateContribution struct { | |
Edges []IssueEdge `json:"edges"` | |
} | |
type ContributionsCollection struct { | |
PullRequestReviewContributions ReviewContributions `json:"pullRequestReviewContributions"` | |
PullRequestContributions PullRequestContributions `json:"pullRequestContributions"` | |
IssueContributions IssueCreateContribution `json:"issueContributions"` | |
} | |
type IssueCommentNode struct { | |
Issue Issue `json:"issue"` | |
CreatedAt time.Time `json:"createdAt"` | |
} | |
type IssueCommentEdge struct { | |
Node IssueCommentNode `json:"node"` | |
} | |
type IssueComments struct { | |
Edges []IssueCommentEdge `json:"edges"` | |
} | |
type Viewer struct { | |
ContributionsCollection ContributionsCollection `json:"contributionsCollection"` | |
IssueComments IssueComments `json:"issueComments"` | |
} | |
type Data struct { | |
Viewer Viewer `json:"viewer"` | |
} | |
type Resp struct { | |
Data Data `json:"data"` | |
} | |
func parseUrl(title, url string) string { | |
parts := strings.Split(url, "/") | |
var name string | |
if parts[3] == "tikv" && parts[4] == "tikv" { | |
name = fmt.Sprintf("%s", parts[6]) | |
} else { | |
name = fmt.Sprintf("%s/%s#%s", parts[3], parts[4], parts[6]) | |
} | |
return fmt.Sprintf("<a href=\"%s\" title=\"%s\">%s</a>", url, title, name) | |
} | |
func inferDate() (from, to time.Time) { | |
year, month, day := time.Now().Date() | |
today := time.Date(year, month, day, 0, 0, 0, 0, time.Local) | |
if *offset == 0 { | |
from = today | |
} else { | |
// Magic go! | |
from = today.Add(time.Duration(-(*offset)) * 24 * time.Hour) | |
} | |
to = from.Add(24 * time.Hour) | |
return | |
} | |
func requestContributions(from, to time.Time) *Resp { | |
// Contributions are collected by calendar day. So it has to be UTC time. | |
requestFrom := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC) | |
from_str := requestFrom.Format(time.RFC3339) | |
log.Println("requesting ", from_str) | |
query := fmt.Sprintf(graphql, from_str, from_str) | |
request_body, err := json.Marshal((map[string]string{ | |
"query": query, | |
})) | |
if err != nil { | |
log.Fatal(err) | |
} | |
if len(githubToken) == 0 { | |
var found bool | |
githubToken, found = os.LookupEnv("GITHUB_TOKEN") | |
if !found { | |
if len(*githubTokenFile) != 0 { | |
f, err := os.Open(*githubTokenFile) | |
if err != nil { | |
log.Fatal(err) | |
} | |
defer f.Close() | |
githubTokenBytes, err := ioutil.ReadAll(f) | |
if err != nil { | |
log.Fatal(err) | |
} | |
githubToken = strings.TrimSpace(string(githubTokenBytes)) | |
} | |
} | |
} | |
if len(githubToken) == 0 { | |
log.Fatal("Set githubToken value first! You can create one by visiting https://github.com/settings/tokens.") | |
} | |
request, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewBuffer(request_body)) | |
if err != nil { | |
log.Fatal(err) | |
} | |
request.Header.Set("Content-Type", "application/json") | |
request.Header.Set("Authorization", fmt.Sprintf("bearer %s", githubToken)) | |
client := http.Client{ | |
Timeout: time.Duration(5 * time.Second), | |
} | |
rawResp, err := client.Do(request) | |
if err != nil { | |
log.Fatal(err) | |
} | |
defer rawResp.Body.Close() | |
body, err := ioutil.ReadAll(rawResp.Body) | |
if err != nil { | |
log.Fatal(err) | |
} | |
resp := Resp{} | |
err = json.Unmarshal(body, &resp) | |
if err != nil { | |
log.Fatal(err) | |
} | |
return &resp | |
} | |
func generateReviewContribution(resp *Resp, output *os.File, from, to time.Time) { | |
reviewData := map[string]bool{} | |
for _, reviewNode := range resp.Data.Viewer.ContributionsCollection.PullRequestReviewContributions.Edges { | |
if reviewNode.Node.OccurredAt.Before(from) || !reviewNode.Node.OccurredAt.Before(to) { | |
log.Printf("skipping %s", reviewNode.Node.OccurredAt.Format(time.RFC3339)) | |
continue | |
} | |
reviewData[parseUrl(reviewNode.Node.PullRequest.Title, reviewNode.Node.PullRequest.Url)] = true | |
} | |
for _, issue := range resp.Data.Viewer.IssueComments.Edges { | |
if issue.Node.CreatedAt.Before(from) || !issue.Node.CreatedAt.Before(to) || !strings.Contains(issue.Node.Issue.Url, "/pull/") { | |
continue | |
} | |
reviewData[parseUrl(issue.Node.Issue.Title, issue.Node.Issue.Url)] = true | |
} | |
if len(reviewData) != 0 { | |
prs := []string{} | |
for k := range reviewData { | |
prs = append(prs, k) | |
} | |
fmt.Fprintf(output, "Reviewed PRs: %s<br/>", strings.Join(prs, ", ")) | |
} else { | |
fmt.Fprintf(output, "No PR was reviewed today.<br/>") | |
} | |
} | |
func generatePullRequestContribution(resp *Resp, output *os.File, from, to time.Time) { | |
prData := []string{} | |
for _, pr := range resp.Data.Viewer.ContributionsCollection.PullRequestContributions.Edges { | |
if pr.Node.OccurredAt.Before(from) || !pr.Node.OccurredAt.Before(to) { | |
log.Printf("skipping %s", pr.Node.OccurredAt.Format(time.RFC3339)) | |
continue | |
} | |
prData = append(prData, parseUrl(pr.Node.PullRequest.Title, pr.Node.PullRequest.Url)) | |
} | |
if len(prData) != 0 { | |
fmt.Fprintf(output, "Created PRs: %s<br/>", strings.Join(prData, ", ")) | |
} | |
} | |
func generateCreateIssueContribution(resp *Resp, output *os.File, from, to time.Time) { | |
issueData := []string{} | |
for _, issue := range resp.Data.Viewer.ContributionsCollection.IssueContributions.Edges { | |
if issue.Node.OccurredAt.Before(from) || !issue.Node.OccurredAt.Before(to) { | |
log.Printf("skipping %s", issue.Node.OccurredAt.Format(time.RFC3339)) | |
continue | |
} | |
issueData = append(issueData, parseUrl(issue.Node.Issue.Title, issue.Node.Issue.Url)) | |
} | |
if len(issueData) != 0 { | |
fmt.Fprintf(output, "Created Issues: %s<br/>", strings.Join(issueData, ", ")) | |
} | |
} | |
func generateIssueCommentContribution(resp *Resp, output *os.File, from, to time.Time) { | |
commentData := map[string]bool{} | |
for _, issue := range resp.Data.Viewer.IssueComments.Edges { | |
if issue.Node.CreatedAt.Before(from) || !issue.Node.CreatedAt.Before(to) || !strings.Contains(issue.Node.Issue.Url, "issue") { | |
continue | |
} | |
commentData[parseUrl(issue.Node.Issue.Title, issue.Node.Issue.Url)] = true | |
} | |
if len(commentData) != 0 { | |
issues := []string{} | |
for k := range commentData { | |
issues = append(issues, k) | |
} | |
fmt.Fprintf(output, "Participated Issues: %s<br/>", strings.Join(issues, ", ")) | |
} | |
} | |
func openOutput(path string) { | |
var err error | |
switch runtime.GOOS { | |
case "linux": | |
err = exec.Command("xdg-open", outputFile).Start() | |
case "windows": | |
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", outputFile).Start() | |
case "darwin": | |
err = exec.Command("open", outputFile).Start() | |
default: | |
err = fmt.Errorf("unsupported platform") | |
} | |
if err != nil { | |
log.Fatal(err) | |
} | |
} | |
func writeStyle(output *os.File) { | |
fmt.Fprintln(output, "<style> * { font-family: \"Arial\"; font-size: 14.66px; }</style>") | |
} | |
func main() { | |
flag.Usage = func() { | |
fmt.Println("A tool to collect your daily contributions automatically.") | |
fmt.Println("") | |
flag.PrintDefaults() | |
} | |
flag.Parse() | |
from, to := inferDate() | |
resp := requestContributions(from, to) | |
output, err := os.Create(outputFile) | |
if err != nil { | |
log.Fatal(err) | |
} | |
defer output.Close() | |
writeStyle(output) | |
generateReviewContribution(resp, output, from, to) | |
generatePullRequestContribution(resp, output, from, to) | |
generateCreateIssueContribution(resp, output, from, to) | |
generateIssueCommentContribution(resp, output, from, to) | |
openOutput(outputFile) | |
} | |
const graphql = `query { | |
viewer { | |
contributionsCollection(from: "%s", to: "%s") { | |
pullRequestReviewContributions(last: 30) { | |
edges { | |
node { | |
pullRequest { | |
title | |
url | |
} | |
occurredAt | |
} | |
} | |
} | |
pullRequestContributions(last: 30) { | |
edges { | |
node { | |
pullRequest { | |
title | |
url | |
} | |
occurredAt | |
} | |
} | |
} | |
issueContributions(last:30) { | |
edges { | |
node { | |
issue { | |
title | |
url | |
} | |
occurredAt | |
} | |
} | |
} | |
} | |
issueComments(last:50) { | |
edges { | |
node { | |
issue { | |
title | |
url | |
} | |
createdAt | |
} | |
} | |
} | |
} | |
}` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment