Skip to content

Instantly share code, notes, and snippets.

@BusyJay
Last active December 26, 2023 07:16
Show Gist options
  • Save BusyJay/cfd0bfd89d87e03f72bcd70284c50d4a to your computer and use it in GitHub Desktop.
Save BusyJay/cfd0bfd89d87e03f72bcd70284c50d4a to your computer and use it in GitHub Desktop.
An easy script to count your work on Github.
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