GitHub Commit File Downloader

Allows you to download individual files or all files as ZIP directly from commit pages.

2025/05/13のページです。最新版はこちら

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
  1. // ==UserScript==
  2. // @name GitHub Commit File Downloader
  3. // @description Allows you to download individual files or all files as ZIP directly from commit pages.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.0
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant none
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const sleep = ms => new Promise(r => setTimeout(r, ms));
  19.  
  20. const fileZipIconIndividual = '<svg aria-hidden="true" focusable="false" class="octicon octicon-file-zip" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><g><path d="M1,1.8C1,0.8,1.8,0,2.8,0h7.6c0.5,0,0.9,0.2,1.2,0.5l2.9,2.9C14.8,3.8,15,4.2,15,4.7v9.6c0,1-0.8,1.8-1.8,1.8H2.8c-1,0-1.8-0.8-1.8-1.8V1.8z M10.5,1.6c0,0-0.1-0.1-0.2-0.1H2.8c-0.1,0-0.2,0.1-0.2,0.2v12.5c0,0.1,0.1,0.2,0.2,0.2h10.5c0.1,0,0.2-0.1,0.2-0.2V4.7c0-0.1,0-0.1-0.1-0.2" /><path d="M8.7,9.2V5c0-0.4-0.4-0.7-0.7-0.7S7.3,4.7,7.3,5v4.1L5.5,7.5c-0.2-0.4-0.7-0.4-1,0c-0.2,0.2-0.2,0.7,0,1l3,3c0.2,0.2,0.7,0.2,1,0l0,0l3-3c0.2-0.2,0.2-0.7,0-1c-0.2-0.2-0.7-0.2-1,0L8.7,9.2z" /></g></svg>';
  21.  
  22. const fileZipIconAll = '<svg aria-hidden="true" focusable="false" class="octicon octicon-file-zip" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><path d="M3.5 1.75v11.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 2 13.25V1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.185 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 12.25 15h-.5a.75.75 0 0 1 0-1.5h.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177L9.513 1.573a.25.25 0 0 0-.177-.073H7.25a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5h-3a.25.25 0 0 0-.25.25Zm3.75 8.75h.5c.966 0 1.75.784 1.75 1.75v3a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1-.75-.75v-3c0-.966.784-1.75 1.75-1.75ZM6 5.25a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 6 5.25Zm.75 2.25h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 6.75A.75.75 0 0 1 8.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 6.75ZM8.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 9.75A.75.75 0 0 1 8.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 9.75Zm-1 2.5v2.25h1v-2.25a.25.25 0 0 0-.25-.25h-.5a.25.25 0 0 0-.25.25Z"></path></svg>';
  23.  
  24. function createIndividualDownloadButtons() {
  25. const fileRows = document.querySelectorAll('ul[role="tree"] > li[id]');
  26. fileRows.forEach(row => {
  27. const filename = row.id.replace(/\u200E/g, '');
  28. if (row.querySelector('.gh-file-download')) return;
  29.  
  30. const fileIconDiv = row.querySelector('.PRIVATE_TreeView-item-visual');
  31. if (!fileIconDiv) return;
  32.  
  33. fileIconDiv.innerHTML = fileZipIconIndividual;
  34. fileIconDiv.style.cursor = 'pointer';
  35. fileIconDiv.className += ' gh-file-download';
  36. fileIconDiv.style.transition = 'transform 0.15s ease-in-out';
  37. fileIconDiv.addEventListener('mouseenter', () => fileIconDiv.style.transform = 'scale(1.1)');
  38. fileIconDiv.addEventListener('mouseleave', () => fileIconDiv.style.transform = 'scale(1)');
  39.  
  40. fileIconDiv.onclick = async (e) => {
  41. e.stopPropagation();
  42. const [_, user, repo, __, commit] = location.pathname.split('/');
  43. const rawUrl = `https://raw.githubusercontent.com/${user}/${repo}/${commit}/${filename}`;
  44. try {
  45. const res = await fetch(rawUrl);
  46. const blob = await res.blob();
  47. const url = URL.createObjectURL(blob);
  48. const a = document.createElement('a');
  49. a.href = url;
  50. a.download = filename.split('/').pop();
  51. document.body.appendChild(a);
  52. a.click();
  53. a.remove();
  54. URL.revokeObjectURL(url);
  55. } catch (e) {
  56. alert(`Failed to download ${filename}`);
  57. console.error(e);
  58. }
  59. };
  60. });
  61. }
  62.  
  63. function createDownloadAllZipButton() {
  64. const titleEl = document.querySelector('h1[data-component="PH_Title"]');
  65. if (!titleEl || titleEl.querySelector('.gh-download-all-zip')) return;
  66.  
  67. const btn = document.createElement('button');
  68. btn.innerHTML = fileZipIconAll;
  69. btn.className = 'gh-download-all-zip';
  70. btn.style.marginLeft = '12px';
  71. btn.style.cursor = 'pointer';
  72. btn.style.border = 'none';
  73. btn.style.background = 'none';
  74. btn.style.display = 'inline-flex';
  75. btn.style.alignItems = 'center';
  76. btn.style.justifyContent = 'center';
  77. btn.style.padding = '0';
  78. btn.style.transition = 'transform 0.15s ease-in-out';
  79. btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.2)');
  80. btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
  81.  
  82. btn.onclick = async () => {
  83. const [_, user, repo, __, commit] = location.pathname.split('/');
  84. const fileEls = document.querySelectorAll('ul[role="tree"] > li[id]');
  85. if (!fileEls.length) return alert('No files found.');
  86.  
  87. const zip = new JSZip();
  88. for (const li of fileEls) {
  89. const filename = li.id.replace(/\u200E/g, '');
  90. const rawUrl = `https://raw.githubusercontent.com/${user}/${repo}/${commit}/${filename}`;
  91. try {
  92. const res = await fetch(rawUrl);
  93. if (!res.ok) throw new Error(`HTTP ${res.status}`);
  94. const blob = await res.blob();
  95. const arrayBuffer = await blob.arrayBuffer();
  96. zip.file(filename.split('/').pop(), arrayBuffer);
  97. await sleep(200);
  98. } catch (err) {
  99. console.error(`Error downloading ${filename}:`, err);
  100. }
  101. }
  102.  
  103. const content = await zip.generateAsync({ type: 'blob' });
  104. const a = document.createElement('a');
  105. a.href = URL.createObjectURL(content);
  106. a.download = `${repo}-${commit.slice(0, 7)}.zip`;
  107. document.body.appendChild(a);
  108. a.click();
  109. a.remove();
  110. };
  111.  
  112. titleEl.appendChild(btn);
  113. }
  114.  
  115. function handleRouteChange() {
  116. if (!location.pathname.match(/^\/[^\/]+\/[^\/]+\/commit\/[a-f0-9]+$/)) return;
  117. createIndividualDownloadButtons();
  118. createDownloadAllZipButton();
  119. }
  120.  
  121. const observer = new MutationObserver(() => {
  122. handleRouteChange();
  123. });
  124. observer.observe(document.body, { childList: true, subtree: true });
  125.  
  126. (function() {
  127. const origPushState = history.pushState;
  128. const origReplaceState = history.replaceState;
  129. let lastPath = location.pathname;
  130.  
  131. function checkPathChange() {
  132. if (location.pathname !== lastPath) {
  133. lastPath = location.pathname;
  134. setTimeout(handleRouteChange, 100);
  135. }
  136. }
  137.  
  138. history.pushState = function(...args) {
  139. origPushState.apply(this, args);
  140. checkPathChange();
  141. };
  142.  
  143. history.replaceState = function(...args) {
  144. origReplaceState.apply(this, args);
  145. checkPathChange();
  146. };
  147.  
  148. window.addEventListener('popstate', checkPathChange);
  149. })();
  150.  
  151. handleRouteChange();
  152. })();