GitHub Repo Size Display

Display repository size on GitHub repo pages

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub Repo Size Display
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Display repository size on GitHub repo pages
// @license      MIT
// @author       Lainbo
// @match        https://github.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHdpZHRoPSIxMSIgaGVpZ2h0PSIxMyIgeD0iMS41IiB5PSIuNSIgcng9IjEiLz48cGF0aCBkPSJtNC41IDEwLjVsMS43NS0yLjc1TTQuMDkgN0EyLjkzIDIuOTMgMCAwIDEgNCA1LjE2YTMgMyAwIDEgMSA0LjY3IDMuMjdNNy41IDExSDEwIi8+PC9nPjwvc3ZnPg==
// ==/UserScript==

(function () {
  'use strict'

  GM_addStyle(`
    .modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(0, 0, 0, 0.5);
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 9999;
    }
    .modal-content {
        background-color: #f6f8fa;
        padding: 24px;
        border-radius: 6px;
        width: 340px;
        box-shadow: 0 8px 24px rgba(140,149,159,0.2);
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    }
    .modal-title {
        font-size: 20px;
        font-weight: 600;
        margin-bottom: 16px;
        color: #24292f;
    }
    .modal-description {
        font-size: 14px;
        color: #57606a;
        margin-bottom: 16px;
        line-height: 1.5;
    }
    .modal-content input {
        width: 100%;
        margin-bottom: 16px;
        padding: 5px 12px;
        font-size: 14px;
        line-height: 20px;
        color: #24292f;
        vertical-align: middle;
        background-color: #ffffff;
        background-repeat: no-repeat;
        background-position: right 8px center;
        border: 1px solid #d0d7de;
        border-radius: 6px;
        box-shadow: inset 0 1px 0 rgba(208,215,222,0.2);
    }
    .modal-content button {
        color: #ffffff;
        background-color: #2da44e;
        padding: 5px 16px;
        font-size: 14px;
        font-weight: 500;
        line-height: 20px;
        white-space: nowrap;
        vertical-align: middle;
        cursor: pointer;
        border: 1px solid;
        border-radius: 6px;
        appearance: none;
        user-select: none;
        margin-right: 8px;
    }
    .modal-content button.cancel {
        color: #24292f;
        background-color: #f6f8fa;
        border-color: rgba(27,31,36,0.15);
    }
    .modal-content button:hover {
        background-color: #2c974b;
    }
    .modal-content button.cancel:hover {
        background-color: #f3f4f6;
    }
    .repo-size-label.error {
        background-color: #ffdce0;
        color: #cf222e;
    }
`)

  let observer
  let repoTitleComponent

  function formatSize(sizeInKB) {
    if (sizeInKB >= 1024 * 1024) {
      return `${(sizeInKB / (1024 * 1024)).toFixed(2)} GB`
    }
    else if (sizeInKB >= 1024) {
      return `${(sizeInKB / 1024).toFixed(2)} MB`
    }
    else {
      return `${sizeInKB} KB`
    }
  }

  function updateRepoSize(content, isError = false) {
    if (!repoTitleComponent) {
      repoTitleComponent = document.querySelector('#repo-title-component')
      if (!repoTitleComponent) {
        return
      }
    }

    let sizeElement = repoTitleComponent.querySelector('.repo-size-label')
    if (!sizeElement) {
      sizeElement = document.createElement('span')
      sizeElement.className = 'Label Label--secondary v-align-middle ml-1 repo-size-label'
      repoTitleComponent.appendChild(sizeElement)
    }

    sizeElement.textContent = content
    sizeElement.classList.toggle('error', isError)
  }

  function fetchRepoSize() {
    const repoRegex = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/
    const match = location.href.match(repoRegex)

    if (!match) { return }

    const [, owner, repo] = match
    const apiUrl = `https://api.github.com/repos/${owner}/${repo}`
    const token = GM_getValue('github_token', '')

    if (!token) {
      updateRepoSize('Token not set', true)
      return
    }

    fetch(apiUrl, {
      headers: { Authorization: `token ${token}` },
    })
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`)
        }
        return response.json()
      })
      .then((data) => {
        if (data.size !== undefined) {
          updateRepoSize(formatSize(data.size))
        }
        else {
          throw new Error('Size information not available')
        }
      })
      .catch((error) => {
        console.error('Error fetching repo size:', error)
        updateRepoSize('Size fetch failed', true)
      })
  }

  function createModal() {
    const tokenCreationUrl = new URL('https://github.com/settings/tokens/new')
    tokenCreationUrl.searchParams.append('description', 'GitHub Repo Size Display')
    tokenCreationUrl.searchParams.append('scopes', 'repo')

    const modalHTML = `
        <div class="modal-overlay">
            <div class="modal-content">
                <h2 class="modal-title">Set GitHub Token</h2>
                <p class="modal-description">
                    Enter your GitHub personal access token with "repo" scope.
                    <a href="${tokenCreationUrl.toString()}" target="_blank" rel="noopener noreferrer">
                        Click here to create a new token
                    </a>
                </p>
                <input type="text" id="github-token-input" placeholder="Enter your GitHub personal access token">
                <button id="save-token">Save</button>
                <button id="cancel-token" class="cancel">Cancel</button>
            </div>
        </div>
    `

    const modalContainer = document.createElement('div')
    modalContainer.innerHTML = modalHTML
    document.body.appendChild(modalContainer)

    const input = document.getElementById('github-token-input')
    input.value = GM_getValue('github_token', '')

    document.getElementById('save-token').addEventListener('click', () => {
      const token = input.value.trim()
      if (token) {
        GM_setValue('github_token', token)
        modalContainer.remove()
        location.reload()
      }
    })

    document.getElementById('cancel-token').addEventListener('click', () => modalContainer.remove())
  }

  function observePageChanges() {
    const targetNode = document.body
    const config = { childList: true, subtree: true }

    observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
          const newRepoTitleComponent = document.querySelector('#repo-title-component')
          if (newRepoTitleComponent && newRepoTitleComponent !== repoTitleComponent) {
            repoTitleComponent = newRepoTitleComponent
            fetchRepoSize()
            break
          }
        }
      }
    })

    observer.observe(targetNode, config)
  }

  function init() {
    observePageChanges()
  }

  // Register the menu command to set the GitHub token
  GM_registerMenuCommand('Set GitHub Token', createModal)

  init()
})()