Skip to content

Commit

Permalink
🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
inokawa committed Jan 31, 2021
1 parent eb2c7b0 commit 31c225b
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/run.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: update gist

on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

jobs:
run:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2

- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "14.x"

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: sudo apt-get install cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev
- run: sudo gem install github-linguist
- run: npm install
- run: npm start
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIST_ID: 64dacee1c6c93cdbcf48548f6598f823
USERNAME: ${{ github.actor }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 inokawa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<p align="center">
<img width="400" src="./example.png">
<h3 align="center">lang-box</h3>
<p align="center">💻 Update a pinned gist to contain languages of your recent commits in GitHub</p>
</p>

---

> This project is inspired by [waka-box](https://github.com/matchai/waka-box), [productive-box](https://github.com/maxam2017/productive-box) and [metrics](https://github.com/lowlighter/metrics).<br/>
> 📌✨ For more pinned-gist projects like this one, check out: https://github.com/matchai/awesome-pinned-gists
This project gets your recent commits from your activities fetched from GitHub API, and process them with [linguist](https://github.com/github/linguist) to show the percentage of each languages used. This project also calculate how many lines of codes were added/removed per language.

## Setup

### Prep work

1. Create a new public GitHub Gist (https://gist.github.com/)
1. Create a token with the `gist` scope and copy it. (https://github.com/settings/tokens/new)

- And if you would like to include commits in private repos, also add `repo` scope.

> Enable `repo` scope seems **DANGEROUS**, but secrets are not passed to workflows that are triggered by a pull request from a fork (https://docs.github.com/en/actions/reference/encrypted-secrets)
### Project setup

1. [Create a template repository](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) by clicking [here](https://github.com/inokawa/lang-box/generate), or you can click the **Use this template** button on this project.
- If you added `repo` scope above, it's recommended to create private repository.
1. Open the "Actions" tab of your fork and click the "enable" button.
1. Edit the [environment variable](https://github.com/inokawa/lang-box/blob/master/.github/workflows/run.yml#L32-L33) in `.github/workflows/run.yml`:

- **GIST_ID:** The ID portion from your gist url: `https://gist.github.com/inokawa/`**`64dacee1c6c93cdbcf48548f6598f823`**.

1. Go to the repo **Settings > Secrets**
1. Add the following environment variables:
- **GH_TOKEN:** The GitHub token generated above.
1. [Pin the newly created Gist](https://help.github.com/en/github/setting-up-and-managing-your-github-profile/pinning-items-to-your-profile)
Binary file added example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "lang-box",
"version": "1.0.0",
"description": "💻 Update a pinned gist to contain languages of your recent commits in GitHub",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node --experimental-modules --es-module-specifier-resolution=node src/index"
},
"dependencies": {
"node-fetch": "2.6.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/inokawa/lang-box.git"
},
"keywords": [],
"author": "inokawa <[email protected]> (https://github.com/inokawa/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/inokawa/lang-box/issues"
},
"homepage": "https://github.com/inokawa/lang-box#readme"
}
40 changes: 40 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fetch from "node-fetch";

export class ApiClient {
constructor(token) {
this.token = token;
}

fetch = async (path, method = "GET", body) => {
const res = await fetch(`https://api.github.com${path}`, {
method,
headers: {
Authorization: `bearer ${this.token}`,
"Content-Type": "application/json",
Accept: "application/vnd.github.v3+json",
},
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
throw new Error(json.message);
}
return json;
};

fetchGq = async (query) => {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `bearer ${this.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }).replace(/\\n/g, ""),
});
const json = await res.json();
if (!res.ok) {
throw new Error(json.message);
}
return json;
};
}
122 changes: 122 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ApiClient } from "./api";
import { createContent } from "./text";
import { runLinguist } from "./linguist";

const { GH_TOKEN, GIST_ID, USERNAME, DAYS } = process.env;

(async () => {
try {
if (!GH_TOKEN) {
throw new Error("GH_TOKEN is not provided.");
}
if (!GIST_ID) {
throw new Error("GIST_ID is not provided.");
}
if (!USERNAME) {
throw new Error("USERNAME is not provided.");
}

const api = new ApiClient(GH_TOKEN);
const username = USERNAME;
const days = Math.max(1, Math.min(30, Number(DAYS || 7)));

console.log(`username is ${username}.`);
console.log(`\n`);

// https://docs.github.com/en/rest/reference/activity
// GitHub API supports 300 events at max and events older than 90 days will not be fetched.
const maxEvents = 300;
const perPage = 100;
const pages = Math.ceil(maxEvents / perPage);
const fromDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000);

const commits = [];
try {
for (let page = 0; page < pages; page++) {
// https://docs.github.com/en/developers/webhooks-and-events/github-event-types#pushevent
const pushEvents = (
await api.fetch(
`/users/${username}/events?per_page=${perPage}&page=${page}`
)
).filter(
({ type, actor }) => type === "PushEvent" && actor.login === username
);

const recentPushEvents = pushEvents.filter(
({ created_at }) => new Date(created_at) > fromDate
);
const isEnd = pushEvents.some(
({ created_at }) => !(new Date(created_at) > fromDate)
);
console.log(`${recentPushEvents.length} events fetched.`);

commits.push(
...(
await Promise.allSettled(
recentPushEvents.flatMap(({ repo, payload }) =>
payload.commits
.filter((c) => c.distinct === true)
.map((c) => api.fetch(`/repos/${repo.name}/commits/${c.sha}`))
)
)
)
.filter(({ status }) => status === "fulfilled")
.map(({ value }) => value)
);

if (isEnd) {
break;
}
}
} catch (e) {
console.log("no more page to load");
}

console.log(`${commits.length} commits fetched.`);
console.log(`\n`);

// https://docs.github.com/en/rest/reference/repos#compare-two-commits
const files = commits.flatMap((res) =>
res.files.map(
({
filename,
additions,
deletions,
changes,
status, // added, removed, modified, renamed
patch,
}) => ({
path: filename,
additions,
deletions,
changes,
status,
patch,
})
)
);

const langs = await runLinguist(files);

const content = createContent(langs);
console.log(`\n`);
console.log(content);
console.log(`\n`);

const gist = await api.fetch(`/gists/${GIST_ID}`);
const filename = Object.keys(gist.files)[0];
await api.fetch(`/gists/${GIST_ID}`, "PATCH", {
files: {
[filename]: {
filename: `💻 Recent coding in languages`,
content,
},
},
});

console.log(`Update succeeded.`);
} catch (e) {
console.error(e);
process.exitCode = 1;
}
})();
Loading

0 comments on commit 31c225b

Please sign in to comment.