您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
One-click tool to squash GitHub Pull Requests. Creates a new PR with squashed commits and preserves the description.
当前为
// ==UserScript== // @name GitHub PR Squasher // @namespace https://github.com/balakumardev // @version 1.0.0 // @description One-click tool to squash GitHub Pull Requests. Creates a new PR with squashed commits and preserves the description. // @author Bala Kumar // @license MIT // @match https://github.com/* // @icon https://github.githubassets.com/favicons/favicon.svg // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect api.github.com // @supportURL https://github.com/balakumardev/github-pr-squasher/issues // @homepage https://github.com/balakumardev/github-pr-squasher // ==/UserScript== /* =========================================== GitHub PR Squasher =========================================== A userscript that adds a "Squash & Recreate PR" button to GitHub pull requests. It creates a new PR with squashed commits while preserving the original description. Features: - One-click PR squashing - Preserves PR description - Automatically closes original PR and deletes the branch cleaning up - Secure token storage - Progress indicators Installation: 1. Install Tampermonkey or Greasemonkey 2. Install this script 3. Set your GitHub token (click Tampermonkey icon → Set GitHub Token) 4. Refresh GitHub 5. Look for the "Squash & Recreate PR" button on PR pages GitHub Token Instructions: 1. Go to GitHub Settings → Developer Settings → Personal Access Tokens → Tokens (classic) 2. Generate new token (classic) 3. Select 'repo' permission 4. Copy token and paste it in the script settings Support: [email protected] */ (function() { 'use strict'; const DEBUG = true; // Add settings menu to Tampermonkey GM_registerMenuCommand('Set GitHub Token', async () => { const token = prompt('Enter your GitHub Personal Access Token (Classic):', GM_getValue('github_token', '')); if (token !== null) { if (token.startsWith('ghp_')) { await GM_setValue('github_token', token); alert('Token saved! Please refresh the page.'); } else { alert('Invalid token format. Token should start with "ghp_"'); } } }); function debugLog(...args) { if (DEBUG) console.log('[PR Squasher]', ...args); } async function getGitHubToken() { const token = GM_getValue('github_token'); if (!token) { throw new Error('GitHub token not set. Click on the Tampermonkey icon and select "Set GitHub Token"'); } return token; } async function githubAPI(endpoint, method = 'GET', body = null) { debugLog(`API Call: ${method} ${endpoint}`); if (body) debugLog('Request Body:', body); const token = await getGitHubToken(); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: `https://api.github.com${endpoint}`, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json', }, data: body ? JSON.stringify(body) : null, onload: function(response) { debugLog(`Response ${endpoint}:`, { status: response.status, statusText: response.statusText, responseText: response.responseText }); if (response.status >= 200 && response.status < 300 || (method === 'DELETE' && response.status === 404)) { // Allow 404 for DELETE operations as the resource might already be gone resolve(response.responseText ? JSON.parse(response.responseText) : {}); } else { reject(new Error(`GitHub API error: ${response.status} - ${response.responseText}`)); } }, onerror: function(error) { debugLog('Request failed:', error); reject(error); } }); }); } async function handleSquash() { const button = document.getElementById('squash-button'); button.disabled = true; button.innerHTML = '⏳ Starting...'; try { // Verify token exists await getGitHubToken(); // Step 1: Get basic PR info const prInfo = { owner: window.location.pathname.split('/')[1], repo: window.location.pathname.split('/')[2], prNumber: window.location.pathname.split('/')[4], branch: document.querySelector('.head-ref').innerText.trim(), title: document.querySelector('.js-issue-title').innerText.trim(), baseBranch: document.querySelector('.base-ref').innerText.trim(), description: document.querySelector('.comment-body')?.innerText.trim() || '' }; debugLog('PR Info:', prInfo); // Step 2: Get PR details button.innerHTML = '⏳ Getting PR details...'; const prDetails = await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/pulls/${prInfo.prNumber}`); debugLog('PR Details:', prDetails); // Step 3: Get the head commit's tree button.innerHTML = '⏳ Getting tree...'; const headCommit = await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/git/commits/${prDetails.head.sha}`); debugLog('Head Commit:', headCommit); // Step 4: Create new branch name const timestamp = new Date().getTime(); const newBranchName = `squashed-pr-${prInfo.prNumber}-${timestamp}`; debugLog('New Branch Name:', newBranchName); // Step 5: Create new branch from base button.innerHTML = '⏳ Creating new branch...'; await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/git/refs`, 'POST', { ref: `refs/heads/${newBranchName}`, sha: prDetails.base.sha }); // Step 6: Create squashed commit button.innerHTML = '⏳ Creating squashed commit...'; const newCommit = await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/git/commits`, 'POST', { message: `${prInfo.title}\n\nSquashed commits from #${prInfo.prNumber}`, tree: headCommit.tree.sha, parents: [prDetails.base.sha] }); debugLog('New Commit:', newCommit); // Step 7: Update branch reference button.innerHTML = '⏳ Updating branch...'; await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/git/refs/heads/${newBranchName}`, 'PATCH', { sha: newCommit.sha, force: true }); // Step 8: Create new PR button.innerHTML = '⏳ Creating new PR...'; const newPR = await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/pulls`, 'POST', { title: `${prInfo.title} (Squashed)`, head: newBranchName, base: prInfo.baseBranch, body: `${prInfo.description}\n\n---\n_Squashed version of #${prInfo.prNumber}_` }); // Step 9: Close original PR button.innerHTML = '⏳ Cleaning up...'; await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/pulls/${prInfo.prNumber}`, 'PATCH', { state: 'closed' }); // Step 10: Delete original branch try { await githubAPI(`/repos/${prInfo.owner}/${prInfo.repo}/git/refs/heads/${prInfo.branch}`, 'DELETE'); debugLog('Deleted original branch:', prInfo.branch); } catch (error) { debugLog('Failed to delete original branch:', error); // Continue even if branch deletion fails } // Success! Redirect to new PR window.location.href = newPR.html_url; } catch (error) { console.error('Failed to squash PR:', error); debugLog('Error details:', error); alert(`Failed to squash PR: ${error.message}\nCheck console for details`); button.disabled = false; button.innerHTML = '🔄 Squash & Recreate PR'; } } function addSquashButton() { if (window.location.href.includes('/pull/')) { const actionBar = document.querySelector('.gh-header-actions'); if (actionBar && !document.getElementById('squash-button')) { const squashButton = document.createElement('button'); squashButton.id = 'squash-button'; squashButton.className = 'btn btn-sm btn-primary'; squashButton.innerHTML = '🔄 Squash & Recreate PR'; squashButton.onclick = handleSquash; actionBar.appendChild(squashButton); } } } // Add button when page loads addSquashButton(); // Add button when navigation occurs const observer = new MutationObserver(() => { if (window.location.href.includes('/pull/')) { addSquashButton(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();