GitHub Repo Size

Displays repository size.

As of 2025-05-14. See the latest version.

// ==UserScript==
// @name         GitHub Repo Size
// @description  Displays repository size.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://github.com/*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.codetabs.com
// @connect      api.cors.lol
// @connect      api.allorigins.win
// @connect      everyorigin.jwvbremen.nl
// @connect      api.github.com
// ==/UserScript==

;(() => {
    let isRequestInProgress = false
    let debounceTimer = null
    const CACHE_DURATION = 10 * 60 * 1000
  
    const databaseIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" class="octicon mr-2" fill="currentColor" aria-hidden="true" style="vertical-align: text-bottom;">
      <path d="M400 86l0 88.7c-13.3 7.2-31.6 14.2-54.8 19.9C311.3 203 269.5 208 224 208s-87.3-5-121.2-13.4C79.6 188.9 61.3 182 48 174.7L48 86l.6-.5C53.9 81 64.5 74.8 81.8 68.6C115.9 56.5 166.2 48 224 48s108.1 8.5 142.2 20.6c17.3 6.2 27.8 12.4 33.2 16.9l.6 .5zm0 141.5l0 75.2c-13.3 7.2-31.6 14.2-54.8 19.9C311.3 331 269.5 336 224 336s-87.3-5-121.2-13.4C79.6 316.9 61.3 310 48 302.7l0-75.2c13.3 5.3 27.9 9.9 43.3 13.7C129.5 250.6 175.2 256 224 256s94.5-5.4 132.7-14.8c15.4-3.8 30-8.3 43.3-13.7zM48 426l0-70.4c13.3 5.3 27.9 9.9 43.3 13.7C129.5 378.6 175.2 384 224 384s94.5-5.4 132.7-14.8c15.4-3.8 30-8.3 43.3-13.7l0 70.4-.6 .5c-5.3 4.5-15.9 10.7-33.2 16.9C332.1 455.5 281.8 464 224 464s-108.1-8.5-142.2-20.6c-17.3-6.2-27.8-12.4-33.2-16.9L48 426z"/>
  </svg>`
  
    const proxyServices = [
      {
        name: "Direct GitHub API",
        url: "https://api.github.com/repos/",
        parseResponse: (response) => {
          return JSON.parse(response)
        },
      },
      {
        name: "CodeTabs Proxy",
        url: "https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/",
        parseResponse: (response) => {
          return JSON.parse(response)
        },
      },
      {
        name: "CORS.lol Proxy",
        url: "https://api.cors.lol/?url=https://api.github.com/repos/",
        parseResponse: (response) => {
          return JSON.parse(response)
        },
      },
      {
        name: "AllOrigins Proxy",
        url: "https://api.allorigins.win/get?url=https://api.github.com/repos/",
        parseResponse: (response) => {
          const parsed = JSON.parse(response)
          return JSON.parse(parsed.contents)
        },
      },
      {
        name: "EveryOrigin Proxy",
        url: "https://everyorigin.jwvbremen.nl/api/get?url=https://api.github.com/repos/",
        parseResponse: (response) => {
          const parsed = JSON.parse(response)
          return JSON.parse(parsed.html)
        },
      },
    ]
  
    function extractRepoInfo() {
      const match = window.location.pathname.match(/^\/([^/]+)\/([^/]+)(\/|$)/)
      if (!match) return null
  
      return {
        owner: match[1],
        repo: match[2],
      }
    }
  
    function formatSize(bytes) {
      const units = ["B", "KB", "MB", "GB", "TB"]
      let i = 0
      while (bytes >= 1024 && i < units.length - 1) {
        bytes /= 1024
        i++
      }
      return {
        value: bytes.toFixed(1),
        unit: units[i],
      }
    }
  
    function injectSize({ value, unit }, downloadURL) {
      const existingSizeDiv = document.querySelector(".gh-repo-size-display")
      if (existingSizeDiv) {
        existingSizeDiv.remove()
      }
  
      const forksHeader = Array.from(document.querySelectorAll("h3.sr-only")).find(
        (el) => el.textContent.trim() === "Forks",
      )
      if (!forksHeader) return
  
      const forksContainer = forksHeader.nextElementSibling
      if (!forksContainer || !forksContainer.classList.contains("mt-2")) return
  
      const existingLink = document.querySelector(".Link--muted .octicon-repo-forked")
      if (existingLink) {
        const parentLinkElement = existingLink.closest("a")
  
        const sizeDiv = document.createElement("div")
        sizeDiv.className = "mt-2 gh-repo-size-display"
  
        const downloadLink = document.createElement("a")
        downloadLink.className = parentLinkElement.className
        downloadLink.href = downloadURL
        downloadLink.style.cursor = "pointer"
        downloadLink.title = "Click to download repository as ZIP"
  
        downloadLink.innerHTML = `
    ${databaseIcon}
    <strong>${value}</strong> ${unit}`
  
        sizeDiv.appendChild(downloadLink)
  
        forksContainer.insertAdjacentElement("afterend", sizeDiv)
      } else {
        const sizeDiv = document.createElement("div")
        sizeDiv.className = "mt-2 gh-repo-size-display"
  
        sizeDiv.innerHTML = `
  <a class="Link Link--muted" href="${downloadURL}" title="Click to download repository as ZIP" style="cursor: pointer;">
    ${databaseIcon}
    <strong>${value}</strong> ${unit}
  </a>`
  
        forksContainer.insertAdjacentElement("afterend", sizeDiv)
      }
    }
  
    function getCacheKey(owner, repo) {
      return `gh_repo_size_${owner}_${repo}`
    }
  
    function getFromCache(owner, repo) {
      try {
        const cacheKey = getCacheKey(owner, repo)
        const cachedData = GM_getValue(cacheKey)
        
        if (!cachedData) return null
        
        const { data, timestamp } = cachedData
        const now = Date.now()
        
        if (now - timestamp < CACHE_DURATION) {
          return data
        }
        
        return null
      } catch (error) {
        console.error('Error getting from cache:', error)
        return null
      }
    }
  
    function saveToCache(owner, repo, data) {
      try {
        const cacheKey = getCacheKey(owner, repo)
        GM_setValue(cacheKey, {
          data,
          timestamp: Date.now()
        })
      } catch (error) {
        console.error('Error saving to cache:', error)
      }
    }
  
    async function fetchFromApi(proxyService, owner, repo) {
      const apiUrl = `${proxyService.url}${owner}/${repo}`
  
      return new Promise((resolve) => {
        if (typeof GM_xmlhttpRequest === "undefined") {
          resolve({ success: false, error: "GM_xmlhttpRequest is not defined" })
          return
        }
  
        GM_xmlhttpRequest({
          method: "GET",
          url: apiUrl,
          headers: {
            Accept: "application/vnd.github.v3+json",
          },
          onload: (response) => {
            if (response.responseText.includes("limit") && response.responseText.includes("API")) {
              resolve({
                success: false,
                error: "Rate limit exceeded",
                isRateLimit: true,
              })
              return
            }
  
            if (response.status >= 200 && response.status < 300) {
              try {
                const data = proxyService.parseResponse(response.responseText)
                resolve({ success: true, data: data })
              } catch (e) {
                resolve({ success: false, error: "JSON parse error" })
              }
            } else {
              resolve({
                success: false,
                error: `Status ${response.status}`,
              })
            }
          },
          onerror: () => {
            resolve({ success: false, error: "Network error" })
          },
          ontimeout: () => {
            resolve({ success: false, error: "Timeout" })
          },
        })
      })
    }
  
    async function fetchRepoInfo(owner, repo) {
      if (isRequestInProgress) {
        return
      }
      
      const cachedData = getFromCache(owner, repo)
      if (cachedData) {
        processRepoData(cachedData)
        return
      }
  
      isRequestInProgress = true
      let fetchSuccessful = false
  
      try {
        for (let i = 0; i < proxyServices.length; i++) {
          const proxyService = proxyServices[i]
          const result = await fetchFromApi(proxyService, owner, repo)
  
          if (result.success) {
            saveToCache(owner, repo, result.data)
            processRepoData(result.data)
            fetchSuccessful = true
            break
          }
        }
        
        if (!fetchSuccessful) {
          console.warn('All proxy attempts failed for', owner, repo)
        }
      } finally {
        isRequestInProgress = false
      }
    }
  
    function processRepoData(data) {
      if (data && data.size != null) {
        const repoInfo = extractRepoInfo()
        if (!repoInfo) return
  
        const formatted = formatSize(data.size * 1024)
  
        let defaultBranch = "master"
        if (data.default_branch) {
          defaultBranch = data.default_branch
        }
  
        const downloadURL = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/archive/refs/heads/${defaultBranch}.zip`
        injectSize(formatted, downloadURL)
      }
    }
  
    function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) {
      const repoInfo = extractRepoInfo()
      if (!repoInfo) return
  
      fetchRepoInfo(repoInfo.owner, repoInfo.repo).catch((err) => {
        if (retryCount < maxRetries) {
          const delay = Math.pow(2, retryCount) * 500
          setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay)
        }
      })
    }
  
    function handleRouteChange() {
      const repoInfo = extractRepoInfo()
      if (!repoInfo) return
  
      const pathParts = window.location.pathname.split("/").filter(Boolean)
      if (pathParts.length !== 2) return
  
      if (debounceTimer) {
        clearTimeout(debounceTimer)
      }
  
      debounceTimer = setTimeout(() => {
        checkAndInsertWithRetry()
      }, 300)
    }
  
    const observer = new MutationObserver(() => {
      handleRouteChange()
    })
  
    observer.observe(document.body, { childList: true, subtree: true })
    ;(() => {
      const origPushState = history.pushState
      const origReplaceState = history.replaceState
      let lastPath = location.pathname
  
      function checkPathChange() {
        if (location.pathname !== lastPath) {
          lastPath = location.pathname
          setTimeout(handleRouteChange, 300)
        }
      }
  
      history.pushState = function (...args) {
        origPushState.apply(this, args)
        checkPathChange()
      }
  
      history.replaceState = function (...args) {
        origReplaceState.apply(this, args)
        checkPathChange()
      }
  
      window.addEventListener("popstate", checkPathChange)
    })()
  
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", handleRouteChange)
    } else {
      handleRouteChange()
    }
  })()
长期地址
遇到问题?请前往 GitHub 提 Issues。