├── .gitignore ├── banner.png ├── README.md ├── package.json ├── .github └── workflows │ └── run.yml ├── LICENSE └── src ├── fetch.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .env -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/HR/master/banner.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | neogent 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HR", 3 | "version": "1.0.0", 4 | "description": "HR", 5 | "author": "HR", 6 | "license": "MIT", 7 | "repository": "HR/HR", 8 | "main": "src/index.js", 9 | "scripts": { 10 | "start": "node src/index.js" 11 | }, 12 | "bin": { 13 | "HR": "src/index.js" 14 | }, 15 | "dependencies": { 16 | "@octokit/request": "^6.2.2", 17 | "axios": "^0.27.2", 18 | "dotenv": "^16.0.3", 19 | "numeral": "^2.0.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/run.yml: -------------------------------------------------------------------------------- 1 | name: Update GitHub Stats Gist 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 */12 * * *' 6 | push: 7 | branches: master 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-node@master 13 | with: 14 | node-version: 20 15 | - uses: actions/checkout@master 16 | - run: npm ci 17 | - run: npm start 18 | env: 19 | GH_TOKEN: ${{ secrets.GH_TOKEN }} # Do not edit, defined in secrets 20 | # Edit the following environment variables 21 | GIST_ID: e0af6466e24b01cf35ec8b6d8146aa6a # The ID portion from your gist url 22 | ALL_COMMITS: true # If `true` it will count all commits, if `false` it will count your last year commits 23 | K_FORMAT: false # If `true`, large numbers will be formatted with a "k", for example "1.5k" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Habib Rehman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/fetch.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const userInfoFetcher = (token) => { 4 | return axios({ 5 | url: 'https://api.github.com/graphql', 6 | method: 'post', 7 | headers: { 8 | Authorization: `bearer ${token}`, 9 | }, 10 | data: { 11 | query: ` 12 | query userInfo { 13 | viewer { 14 | name 15 | login 16 | contributionsCollection { 17 | totalCommitContributions 18 | } 19 | repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { 20 | totalCount 21 | } 22 | pullRequests(first: 1) { 23 | totalCount 24 | } 25 | issues(first: 1) { 26 | totalCount 27 | } 28 | repositories(first: 100, ownerAffiliations: OWNER, isFork: false, orderBy: {direction: DESC, field: STARGAZERS}) { 29 | totalCount 30 | nodes { 31 | stargazers { 32 | totalCount 33 | } 34 | forkCount 35 | watchers { 36 | totalCount 37 | } 38 | } 39 | } 40 | } 41 | }`, 42 | }, 43 | }); 44 | }; 45 | 46 | // Experimental API 47 | const totalCommitsFetcher = async (login, token) => { 48 | return axios({ 49 | method: 'get', 50 | url: `https://api.github.com/search/commits?q=author:${login}`, 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | Accept: 'application/vnd.github.cloak-preview', 54 | Authorization: `bearer ${token}`, 55 | }, 56 | }).then((res) => res.data.total_count); 57 | }; 58 | 59 | module.exports = { 60 | userInfoFetcher, 61 | totalCommitsFetcher, 62 | }; 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | require('dotenv').config() 5 | const { request } = require('@octokit/request') 6 | const { userInfoFetcher, totalCommitsFetcher } = require('./fetch') 7 | const numeral = require('numeral') 8 | 9 | const gistId = process.env.GIST_ID 10 | const githubToken = process.env.GH_TOKEN 11 | const countAllCommits = process.env.ALL_COMMITS.toString() === 'true' 12 | const kFormat = process.env.K_FORMAT.toString() === 'true' 13 | 14 | async function main() { 15 | if (!githubToken) { 16 | throw new Error('GH_TOKEN is not defined') 17 | } 18 | let stats 19 | try { 20 | stats = await getStats() 21 | console.info('Successfully fetched statistics from GitHub') 22 | console.info(JSON.stringify(stats, null, 2)) 23 | } catch (e) { 24 | throw new Error(`cannot retrieve statistics: ${e.message}`) 25 | } 26 | try { 27 | await updateGist(stats) 28 | } catch (e) { 29 | throw new Error(`cannot update gist: ${e.message}`) 30 | } 31 | } 32 | 33 | async function getStats() { 34 | const stats = { 35 | name: '', 36 | totalPRs: 0, 37 | totalCommits: 0, 38 | totalIssues: 0, 39 | totalStars: 0, 40 | totalForks: 0, 41 | totalWatchers: 0, 42 | contributedTo: 0, 43 | } 44 | 45 | const user = await userInfoFetcher(githubToken).then( 46 | (res) => res.data.data.viewer 47 | ) 48 | 49 | stats.name = user.name || user.login 50 | stats.totalPRs = user.pullRequests.totalCount 51 | stats.totalIssues = user.issues.totalCount 52 | stats.contributedTo = user.repositoriesContributedTo.totalCount 53 | stats.totalStars = user.repositories.nodes.reduce((prev, curr) => { 54 | return prev + curr.stargazers.totalCount 55 | }, 0) 56 | stats.totalForks = user.repositories.nodes.reduce((prev, curr) => { 57 | return prev + curr.forkCount 58 | }, 0) 59 | stats.totalWatchers = user.repositories.nodes.reduce((prev, curr) => { 60 | return prev + curr.watchers.totalCount 61 | }, 0) 62 | 63 | stats.totalCommits = user.contributionsCollection.totalCommitContributions 64 | if (countAllCommits) { 65 | stats.totalCommits = await totalCommitsFetcher(user.login, githubToken) 66 | } 67 | 68 | return stats 69 | } 70 | 71 | async function updateGist(stats) { 72 | const humanize = (n) => 73 | n >= 1000 ? numeral(n).format(kFormat ? '0.0a' : '0,0') : n 74 | 75 | const gistContent = 76 | [ 77 | ['⭐', `Stars`, humanize(stats.totalStars)], 78 | ['🍴', `Forks`, humanize(stats.totalForks)], 79 | ['👀', `Watchers`, humanize(stats.totalWatchers)], 80 | ['➕', `Commits`, humanize(stats.totalCommits)], 81 | // ['🔀', `PRs`, humanize(stats.totalPRs)], 82 | // ['🚩', `Issues`, humanize(stats.totalIssues)], 83 | ] 84 | .map((content) => { 85 | let line = `${content[1]}:${content[2]}` 86 | line = line.replace(':', ' '.repeat(45 - line.length)) 87 | line = `${content[0]} ${line}` 88 | return line 89 | }) 90 | .join('\n') + '\n' 91 | 92 | const gist = await request('GET /gists/:gist_id', { 93 | gist_id: gistId, 94 | headers: { authorization: `token ${githubToken}` }, 95 | }) 96 | const filename = Object.keys(gist.data.files)[0] 97 | 98 | if (gist.data.files[filename].content === gistContent) { 99 | console.info('Nothing to update') 100 | return 101 | } 102 | 103 | return request('PATCH /gists/:gist_id', { 104 | files: { 105 | [filename]: { 106 | filename: `#`, 107 | content: gistContent, 108 | }, 109 | }, 110 | gist_id: gistId, 111 | headers: { authorization: `token ${githubToken}` }, 112 | }).then(() => { 113 | console.info( 114 | `Updated Gist ${gistId} with the following content:\n${gistContent}` 115 | ) 116 | }) 117 | } 118 | 119 | main().catch((err) => { 120 | console.error(err.message) 121 | process.exit(1) 122 | }) 123 | --------------------------------------------------------------------------------