├── .gitignore ├── screenshot.png ├── icons ├── ghrs16.png ├── ghrs48.png └── ghrs128.png ├── product-hunt.png ├── package.json ├── manifest.json ├── LICENSE.md ├── README.md └── src ├── background.js └── inject.js /.gitignore: -------------------------------------------------------------------------------- 1 | extension.zip 2 | node_modules 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshjv/github-repo-size/HEAD/screenshot.png -------------------------------------------------------------------------------- /icons/ghrs16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshjv/github-repo-size/HEAD/icons/ghrs16.png -------------------------------------------------------------------------------- /icons/ghrs48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshjv/github-repo-size/HEAD/icons/ghrs48.png -------------------------------------------------------------------------------- /product-hunt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshjv/github-repo-size/HEAD/product-hunt.png -------------------------------------------------------------------------------- /icons/ghrs128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshjv/github-repo-size/HEAD/icons/ghrs128.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-repo-size", 3 | "private": true, 4 | "scripts": { 5 | "zip": "bestzip extension.zip src/ icons/ manifest.json LICENSE.md", 6 | "test": "standard" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/stevennoto/github-repo-size.git" 11 | }, 12 | "author": "Harsh Vakharia", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/stevennoto/github-repo-size/issues" 16 | }, 17 | "homepage": "https://github.com/stevennoto/github-repo-size#readme", 18 | "devDependencies": { 19 | "bestzip": "^2.1.4", 20 | "standard": "^12.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHub Repository Size", 3 | "version": "0.6.0", 4 | "manifest_version": 2, 5 | "description": "Automatically adds repository size to GitHub's repository summary", 6 | "homepage_url": "https://github.com/harshjv/github-repo-size", 7 | "author": "Harsh Vakharia", 8 | "icons": { 9 | "16": "icons/ghrs16.png", 10 | "48": "icons/ghrs48.png", 11 | "128": "icons/ghrs128.png" 12 | }, 13 | "permissions": [ 14 | "storage" 15 | ], 16 | "background" : { 17 | "scripts" : [ 18 | "src/background.js" 19 | ] 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "https://github.com/*" 25 | ], 26 | "js": [ 27 | "src/inject.js" 28 | ], 29 | "run_at": "document_end" 30 | } 31 | ], 32 | "browser_action": { 33 | "default_icon": { 34 | "16": "icons/ghrs16.png", 35 | "48": "icons/ghrs48.png", 36 | "128": "icons/ghrs128.png" 37 | }, 38 | "default_title": "GitHub Repository Size: Click to set/remove access token" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Harsh Vakharia 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Chrome extension to display repository size on GitHub [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 2 | 3 | Automatically adds repository size to GitHub's repository summary. 4 | 5 | [![Featured on Product Hunt](./product-hunt.png)](https://www.producthunt.com/tech/github-repository-size) 6 | 7 | 8 | ## Screenshot 9 | 10 | ![Screenshot of repository size on GitHub](https://raw.githubusercontent.com/harshjv/github-repo-size/master/screenshot.png) 11 | 12 | 13 | ## Private Repository 14 | 15 | To enable viewing size of private repositories; 16 | 17 | 1. Install extension from chrome webstore, if you haven't. 18 | 2. Go to https://github.com/settings/tokens to generate your personal access token. 19 | - Check `repo` scope to enable this extension on private repo. 20 | 3. Click on the Github Repo Size extension (this extension)'s icon aside the address bar. 21 | 4. Paste your access token there in the prompt box. 22 | 23 | ### Temporarily override the token 24 | 25 | You can set `x-github-token` in `localStorage` to your access token, and the extension will use this value even if you've previously set token. 26 | 27 | localStorage.setItem('x-github-token', ) 28 | 29 | and then remove it to use previously set token; 30 | 31 | localStorage.removeItem('x-github-token') 32 | 33 | 34 | ## Development 35 | 36 | 1. Clone this repo 37 | 2. Go to chrome extensions [chrome://extensions](chrome://extensions) 38 | 3. Enable developer mode 39 | 4. Click on load unpacked extension and select this cloned repo 40 | 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | /* global chrome, alert, prompt, confirm */ 2 | 3 | const GITHUB_TOKEN_KEY = 'x-github-token' 4 | const TOKEN_FEATURE_INFORMATION_KEY = 'user-knows-token-feature' 5 | 6 | const storage = chrome.storage.sync || chrome.storage.local 7 | 8 | function setGithubToken (key, cb) { 9 | const obj = {} 10 | 11 | obj[GITHUB_TOKEN_KEY] = key 12 | 13 | storage.set(obj, function () { 14 | alert('Your Github token has been set successfully. Reload the Github page to see changes.') 15 | 16 | cb() 17 | }) 18 | } 19 | 20 | function handleOldGithubToken (cb) { 21 | storage.get(GITHUB_TOKEN_KEY, function (storedData) { 22 | const oldGithubToken = storedData[GITHUB_TOKEN_KEY] 23 | 24 | if (oldGithubToken) { 25 | if (confirm('You have already set your Github token. Do you want to remove it?')) { 26 | storage.remove(GITHUB_TOKEN_KEY, function () { 27 | alert('You have successfully removed Github token. Click extension icon again to set a new token.') 28 | 29 | cb(null, false) 30 | }) 31 | } else { 32 | cb(null, false) 33 | } 34 | } else { 35 | cb(null, true) 36 | } 37 | }) 38 | } 39 | 40 | const userNowKnowsAboutGithubTokenFeature = (cb) => { 41 | const obj = {} 42 | obj[TOKEN_FEATURE_INFORMATION_KEY] = true 43 | 44 | storage.set(obj, cb) 45 | } 46 | 47 | function informUserAboutGithubTokenFeature () { 48 | storage.get(TOKEN_FEATURE_INFORMATION_KEY, function (storedData) { 49 | const userKnows = storedData[TOKEN_FEATURE_INFORMATION_KEY] 50 | 51 | if (!userKnows) { 52 | if (confirm('GitHub Repository Size now supports private repositories through Github personal access tokens. Do you want to add a token?')) { 53 | askGithubToken(() => { 54 | userNowKnowsAboutGithubTokenFeature(() => {}) 55 | }) 56 | } else { 57 | userNowKnowsAboutGithubTokenFeature(() => { 58 | alert('You can click extension icon to set a token.') 59 | }) 60 | } 61 | } 62 | }) 63 | } 64 | 65 | const askGithubToken = (cb) => { 66 | const githubToken = prompt('Please enter your Github token') 67 | 68 | if (githubToken === null) return 69 | 70 | if (githubToken) { 71 | setGithubToken(githubToken, cb) 72 | } else { 73 | alert('You have entered an empty token.') 74 | 75 | cb() 76 | } 77 | } 78 | 79 | chrome.browserAction.onClicked.addListener((tab) => { 80 | handleOldGithubToken((_, askToSetToken) => { 81 | if (askToSetToken) { 82 | askGithubToken(() => {}) 83 | } 84 | }) 85 | }) 86 | 87 | informUserAboutGithubTokenFeature() 88 | -------------------------------------------------------------------------------- /src/inject.js: -------------------------------------------------------------------------------- 1 | /* global fetch, Request, Headers, chrome, localStorage */ 2 | 3 | const API = 'https://api.github.com/repos/' 4 | const NAV_ELEM_ID = 'github-repo-size' 5 | const GITHUB_TOKEN_KEY = 'x-github-token' 6 | 7 | const storage = chrome.storage.sync || chrome.storage.local 8 | 9 | let githubToken 10 | 11 | const getRepoObject = () => { 12 | // find file button 13 | const elem = document.querySelector('a.d-none.js-permalink-shortcut') 14 | if (!elem) return false 15 | 16 | if (!elem.href || 17 | !elem.href.match(/^https?:\/\/github.com\//)) { 18 | return false 19 | } 20 | 21 | const repoUri = elem.href.replace(/^https?:\/\/github.com\//, '') 22 | const arr = repoUri.split('/') 23 | 24 | const userOrg = arr.shift() 25 | const repoName = arr.shift() 26 | const repo = `${userOrg}/${repoName}` 27 | 28 | arr.shift() // tree 29 | 30 | const ref = arr.shift() 31 | 32 | return { 33 | repo, 34 | ref, 35 | currentPath: arr.join('/').trim() 36 | } 37 | } 38 | 39 | const getHumanReadableSizeObject = (bytes) => { 40 | if (bytes === 0) { 41 | return { 42 | size: 0, 43 | measure: 'Bytes' 44 | } 45 | } 46 | 47 | const K = 1024 48 | const MEASURE = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 49 | const i = Math.floor(Math.log(bytes) / Math.log(K)) 50 | 51 | return { 52 | size: parseFloat((bytes / Math.pow(K, i)).toFixed(2)), 53 | measure: MEASURE[i] 54 | } 55 | } 56 | 57 | const getHumanReadableSize = (size) => { 58 | if (!size) return '' 59 | 60 | const t = getHumanReadableSizeObject(size) 61 | 62 | return t.size + ' ' + t.measure 63 | } 64 | 65 | const getSizeHTML = (size) => { 66 | const humanReadableSize = getHumanReadableSizeObject(size) 67 | 68 | return ` 69 |
  • 70 | 71 | 74 | ${humanReadableSize.size} ${humanReadableSize.measure} 75 | 76 |
  • 77 | ` 78 | } 79 | 80 | const checkStatus = (response) => { 81 | if (response.status >= 200 && response.status < 300) { 82 | return response 83 | } 84 | 85 | console.error(response) 86 | 87 | throw Error(`GitHub returned an invalid status: ${response.status}`) 88 | } 89 | 90 | const getAPIData = (uri) => { 91 | const headerObj = { 92 | 'User-Agent': 'harshjv/github-repo-size' 93 | } 94 | 95 | const token = localStorage.getItem(GITHUB_TOKEN_KEY) || githubToken 96 | 97 | if (token) { 98 | headerObj.Authorization = 'token ' + token 99 | } 100 | 101 | const request = new Request(`${API}${uri}`, { 102 | headers: new Headers(headerObj) 103 | }) 104 | 105 | return fetch(request) 106 | .then(checkStatus) 107 | .then(response => response.json()) 108 | } 109 | 110 | const getFileName = text => text.trim().split('/')[0] 111 | 112 | const checkForRepoPage = async () => { 113 | const repoObj = getRepoObject() 114 | if (!repoObj) return 115 | 116 | // wait for the table to load 117 | await new Promise((resolve, reject) => { 118 | function loading () { 119 | return !!document.querySelector('div[role="gridcell"] div.Skeleton') 120 | } 121 | 122 | if (!loading()) return resolve() 123 | 124 | const interval = setInterval(() => { 125 | if (!loading()) { 126 | clearInterval(interval) 127 | resolve() 128 | } 129 | }, 100) 130 | }) 131 | 132 | const ns = document.querySelector('ul.UnderlineNav-body') 133 | const navElem = document.getElementById(NAV_ELEM_ID) 134 | const tdElems = document.querySelector('span.github-repo-size-div') 135 | 136 | if (ns && !navElem) { 137 | getAPIData(repoObj.repo).then(summary => { 138 | if (summary && summary.size) { 139 | ns.insertAdjacentHTML('beforeend', getSizeHTML(summary.size * 1024)) 140 | const newLiElem = document.getElementById(NAV_ELEM_ID) 141 | newLiElem.title = 'Click to load directory sizes' 142 | newLiElem.style.cssText = 'cursor: pointer' 143 | newLiElem.onclick = loadDirSizes 144 | } 145 | }) 146 | } 147 | 148 | if (tdElems) return 149 | 150 | const tree = await getAPIData(`${repoObj.repo}/contents/${repoObj.currentPath}?ref=${repoObj.ref}`) 151 | const sizeObj = { '..': '..' } 152 | 153 | tree.forEach(item => { 154 | sizeObj[item.name] = item.type !== 'dir' ? item.size : 'dir' 155 | }) 156 | 157 | const list = [...document.querySelectorAll('div[role="row"].Box-row')] 158 | const items = [...document.querySelectorAll('div[role="row"].Box-row div[role="rowheader"] a')] 159 | const ageForReference = document.querySelectorAll('div[role="row"].Box-row div[role="gridcell"]:last-child') 160 | 161 | items.forEach((item, index) => { 162 | const filename = getFileName(item.text) 163 | const t = sizeObj[filename] 164 | 165 | const div = document.createElement('div') 166 | div.setAttribute('role', 'gridcell') 167 | 168 | div.style.cssText = 'width: 80px' 169 | div.className = 'text-gray-light text-right mr-3' 170 | 171 | let label 172 | 173 | if (t === 'dir') { 174 | label = '···' 175 | div.className += ' github-repo-size-dir' 176 | div.title = 'Click to load directory size' 177 | div.style.cssText = 'cursor: pointer; width: 80px' 178 | div.onclick = loadDirSizes 179 | div.setAttribute('data-dirname', filename) 180 | } else if (t === '..') { 181 | label = '' 182 | } else { 183 | label = getHumanReadableSize(t) 184 | } 185 | 186 | div.innerHTML = `${label}` 187 | 188 | list[index].insertBefore(div, ageForReference[index]) 189 | }) 190 | } 191 | 192 | const loadDirSizes = async () => { 193 | const files = [...document.querySelectorAll('div[role="row"].Box-row div[role="rowheader"] a')] 194 | const dirSizes = [...document.querySelectorAll('div.github-repo-size-dir span')] 195 | const navElem = document.getElementById(NAV_ELEM_ID) 196 | 197 | if (navElem) { 198 | navElem.onclick = null 199 | navElem.title = 'Loading directory sizes...' 200 | } 201 | 202 | dirSizes.forEach(dir => { 203 | dir.textContent = '...' 204 | dir.parentElement.onclick = null 205 | }) 206 | 207 | const repoObj = getRepoObject() 208 | if (!repoObj) return 209 | 210 | const data = await getAPIData(`${repoObj.repo}/git/trees/${repoObj.ref}?recursive=1`) 211 | 212 | if (data.truncated) { 213 | console.warn('github-repo-size: Data truncated. Directory size info may be incomplete.') 214 | } 215 | 216 | const sizeObj = {} 217 | 218 | data.tree.forEach(item => { 219 | if (!item.path.startsWith(repoObj.currentPath)) return 220 | 221 | const arr = item.path 222 | .replace(new RegExp(`^${repoObj.currentPath}`), '') 223 | .replace(/^\//, '') 224 | .split('/') 225 | 226 | if (arr.length >= 2 && item.type === 'blob') { 227 | const dir = arr[0] 228 | if (sizeObj[dir] === undefined) sizeObj[dir] = 0 229 | sizeObj[dir] += item.size 230 | } 231 | }) 232 | 233 | files.forEach(file => { 234 | const dirname = getFileName(file.text) 235 | const t = sizeObj[dirname] 236 | 237 | const dir = dirSizes.find(dir => dir.parentElement.getAttribute('data-dirname') === dirname) 238 | 239 | if (dir) { 240 | dir.textContent = getHumanReadableSize(t) 241 | } 242 | }) 243 | 244 | if (navElem) { 245 | navElem.title = 'Directory sizes loaded successfully' 246 | } 247 | } 248 | 249 | storage.get(GITHUB_TOKEN_KEY, function (data) { 250 | githubToken = data[GITHUB_TOKEN_KEY] 251 | 252 | chrome.storage.onChanged.addListener((changes, namespace) => { 253 | if (changes[GITHUB_TOKEN_KEY]) { 254 | githubToken = changes[GITHUB_TOKEN_KEY].newValue 255 | } 256 | }) 257 | 258 | document.addEventListener('pjax:end', checkForRepoPage, false) 259 | 260 | checkForRepoPage() 261 | }) 262 | --------------------------------------------------------------------------------