Created
July 11, 2025 13:23
-
-
Save freehuntx/16b58832c31273860acb97575b364caa to your computer and use it in GitHub Desktop.
Revisions
-
freehuntx created this gist
Jul 11, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,641 @@ // ==UserScript== // @name GitHub Tag Creator // @namespace http://tampermonkey.net/ // @version 2.2 // @description Add ability to create tags on GitHub via API // @author freehuntx // @match https://github.com/*/*/tags* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; console.log('[GitHub Tag Creator] Script loaded on:', window.location.href); // Add CSS for the tag creator UI GM_addStyle(` .tag-creator-button { margin-left: 16px; } .tag-creator-nav-button { display: inline-block; padding: 5px 16px; font-size: 14px; font-weight: 500; line-height: 20px; white-space: nowrap; vertical-align: middle; cursor: pointer; user-select: none; border: 1px solid; border-radius: 6px; appearance: none; color: var(--color-btn-text); background-color: var(--color-btn-bg); border-color: var(--color-btn-border); box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); transition: 80ms cubic-bezier(0.33, 1, 0.68, 1); transition-property: color, background-color, box-shadow, border-color; } .tag-creator-nav-button:hover { background-color: var(--color-btn-hover-bg); border-color: var(--color-btn-hover-border); } .tag-creator-inline-button { margin-left: 8px; padding: 3px 12px; font-size: 12px; line-height: 20px; color: var(--color-fg-default); background-color: var(--color-btn-bg); border: 1px solid var(--color-btn-border); border-radius: 6px; cursor: pointer; } .tag-creator-inline-button:hover { background-color: var(--color-btn-hover-bg); } .tag-creator-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--color-canvas-default, #ffffff); border: 1px solid var(--color-border-default, #d1d5da); border-radius: 6px; padding: 16px; z-index: 1000; box-shadow: 0 8px 24px rgba(140,149,159,0.2); min-width: 400px; } .tag-creator-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; } .tag-creator-form { display: flex; flex-direction: column; gap: 12px; } .tag-creator-input { padding: 5px 12px; font-size: 14px; line-height: 20px; color: var(--color-fg-default, #24292e); vertical-align: middle; background-color: var(--color-canvas-default, #ffffff); background-repeat: no-repeat; background-position: right 8px center; border: 1px solid var(--color-border-default, #d1d5da); border-radius: 6px; outline: none; box-shadow: inset 0 1px 0 rgba(225,228,232,0.2); } .tag-creator-textarea { min-height: 100px; resize: vertical; } .tag-creator-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; } .tag-creator-error { color: var(--color-danger-fg, #cb2431); font-size: 12px; margin-top: 4px; } .tag-creator-success { color: var(--color-success-fg, #28a745); font-size: 12px; margin-top: 4px; } .tag-creator-close { position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 20px; cursor: pointer; color: var(--color-fg-muted, #586069); padding: 4px 8px; } .tag-creator-close:hover { color: var(--color-fg-default, #24292e); } `); // Function to get repository info from the current page function getRepoInfo() { const pathParts = window.location.pathname.split('/').filter(Boolean); if (pathParts.length >= 2) { return { owner: pathParts[0], repo: pathParts[1] }; } return null; } // Try to extract token from various sources async function getGitHubToken() { // 1. Check if we have a saved token const savedToken = GM_getValue('github_token', ''); if (savedToken) { return savedToken; } // 2. Try to get from GitHub's session (this might work if user has certain browser extensions) try { // Check if we can access GitHub's API with current session const response = await fetch('https://api.github.com/user', { headers: { 'Accept': 'application/vnd.github.v3+json' }, credentials: 'include' }); if (response.ok) { // Session auth might work, but we still need a PAT for API calls console.log('GitHub session detected, but Personal Access Token still required for API operations'); } } catch (e) { console.log('No automatic token detection available'); } // 3. Check for GitHub CLI token (gh) in localStorage try { const ghToken = localStorage.getItem('gh-token'); if (ghToken) { return ghToken; } } catch (e) {} return ''; } // Function to get the default branch async function getDefaultBranch(owner, repo, token) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.github.com/repos/${owner}/${repo}`, headers: { 'Authorization': token ? `token ${token}` : '', 'Accept': 'application/vnd.github.v3+json' }, onload: function(response) { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); resolve(data.default_branch); } else { reject(new Error(`Failed to fetch repository info: ${response.statusText}`)); } }, onerror: function(error) { reject(new Error(`Network error: ${error}`)); } }); }); } // Function to get the latest commit SHA from default branch async function getLatestCommitSHA(owner, repo, token) { const defaultBranch = await getDefaultBranch(owner, repo, token); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`, headers: { 'Authorization': token ? `token ${token}` : '', 'Accept': 'application/vnd.github.v3+json' }, onload: function(response) { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); resolve(data.object.sha); } else { reject(new Error(`Failed to fetch branch info: ${response.statusText}`)); } }, onerror: function(error) { reject(new Error(`Network error: ${error}`)); } }); }); } // Function to create the tag via GitHub API async function createTag(owner, repo, tagName, commitSHA, message, token) { const refUrl = `https://api.github.com/repos/${owner}/${repo}/git/refs`; const tagData = { ref: `refs/tags/${tagName}`, sha: commitSHA }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: refUrl, headers: { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, data: JSON.stringify(tagData), onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(JSON.parse(response.responseText)); } else { let errorMsg = `Failed to create tag: ${response.statusText}`; try { const errorData = JSON.parse(response.responseText); if (errorData.message) { errorMsg = errorData.message; } } catch (e) {} reject(new Error(errorMsg)); } }, onerror: function(error) { reject(new Error(`Network error: ${error}`)); } }); }); } // Function to create annotated tag async function createAnnotatedTag(owner, repo, tagName, commitSHA, message, token) { // First, get user info for tagger details const userInfo = await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://api.github.com/user', headers: { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json' }, onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(JSON.parse(response.responseText)); } else { resolve({ login: 'freehuntx', email: 'freehuntx@users.noreply.github.com' }); } } }); }); const tagUrl = `https://api.github.com/repos/${owner}/${repo}/git/tags`; const tagData = { tag: tagName, message: message, object: commitSHA, type: 'commit', tagger: { name: userInfo.name || userInfo.login || 'freehuntx', email: userInfo.email || `${userInfo.login || 'freehuntx'}@users.noreply.github.com`, date: new Date().toISOString() } }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: tagUrl, headers: { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, data: JSON.stringify(tagData), onload: async function(response) { if (response.status >= 200 && response.status < 300) { const tagObject = JSON.parse(response.responseText); // Now create the reference to this tag object try { await createTag(owner, repo, tagName, tagObject.sha, message, token); resolve(tagObject); } catch (error) { reject(error); } } else { let errorMsg = `Failed to create annotated tag: ${response.statusText}`; try { const errorData = JSON.parse(response.responseText); if (errorData.message) { errorMsg = errorData.message; } } catch (e) {} reject(new Error(errorMsg)); } }, onerror: function(error) { reject(new Error(`Network error: ${error}`)); } }); }); } // Function to show the tag creation modal async function showTagCreatorModal(repoInfo) { // Remove any existing modal const existingModal = document.querySelector('.tag-creator-overlay'); if (existingModal) { existingModal.remove(); } // Try to get saved token const savedToken = await getGitHubToken(); // Create modal HTML with close button const modalHTML = ` <div class="tag-creator-overlay"> <div class="tag-creator-modal"> <button class="tag-creator-close" title="Close">×</button> <h3 style="margin-top: 0;">Create New Tag</h3> <form class="tag-creator-form"> <div> <label for="tag-name">Tag Name:</label> <input type="text" id="tag-name" class="tag-creator-input" placeholder="v1.0.0" required> </div> <div> <label for="commit-sha">Commit SHA:</label> <input type="text" id="commit-sha" class="tag-creator-input" placeholder="Leave empty for latest commit"> </div> <div> <label for="tag-message">Message (optional for annotated tag):</label> <textarea id="tag-message" class="tag-creator-input tag-creator-textarea" placeholder="Tag message..."></textarea> </div> <div> <label for="github-token">GitHub Personal Access Token:</label> <input type="password" id="github-token" class="tag-creator-input" placeholder="ghp_..." required value="${savedToken}"> <small style="color: var(--color-fg-muted);"> Need a token? <a href="https://github.com/settings/tokens/new?scopes=repo" target="_blank">Create one here</a> with 'repo' scope. </small> </div> <div> <label> <input type="checkbox" id="save-token" ${savedToken ? 'checked' : ''}> Save token for future use </label> </div> <div class="tag-creator-error" style="display: none;"></div> <div class="tag-creator-success" style="display: none;"></div> <div class="tag-creator-actions"> <button type="button" class="btn">Cancel</button> <button type="submit" class="btn btn-primary">Create Tag</button> </div> </form> </div> </div> `; // Add modal to page document.body.insertAdjacentHTML('beforeend', modalHTML); // Handle close actions const closeModal = () => { document.querySelector('.tag-creator-overlay').remove(); }; // Close on overlay click document.querySelector('.tag-creator-overlay').addEventListener('click', (e) => { if (e.target.classList.contains('tag-creator-overlay')) { closeModal(); } }); // Close on close button click document.querySelector('.tag-creator-close').addEventListener('click', closeModal); // Close on cancel button click document.querySelector('.tag-creator-form button[type="button"]').addEventListener('click', closeModal); // Handle form submission const form = document.querySelector('.tag-creator-form'); const errorDiv = document.querySelector('.tag-creator-error'); const successDiv = document.querySelector('.tag-creator-success'); form.addEventListener('submit', async (e) => { e.preventDefault(); const tagName = document.getElementById('tag-name').value.trim(); const commitSHA = document.getElementById('commit-sha').value.trim(); const message = document.getElementById('tag-message').value.trim(); const token = document.getElementById('github-token').value.trim(); const saveToken = document.getElementById('save-token').checked; if (!tagName || !token) { errorDiv.textContent = 'Tag name and token are required'; errorDiv.style.display = 'block'; successDiv.style.display = 'none'; return; } // Save token if requested if (saveToken) { GM_setValue('github_token', token); } else { // Clear saved token if unchecked GM_setValue('github_token', ''); } // Disable submit button const submitBtn = form.querySelector('button[type="submit"]'); submitBtn.disabled = true; submitBtn.textContent = 'Creating...'; try { let sha = commitSHA; // If no SHA provided, get the latest commit if (!sha) { sha = await getLatestCommitSHA(repoInfo.owner, repoInfo.repo, token); } // Create the tag if (message) { // Create annotated tag await createAnnotatedTag( repoInfo.owner, repoInfo.repo, tagName, sha, message, token ); } else { // Create lightweight tag await createTag(repoInfo.owner, repoInfo.repo, tagName, sha, '', token); } // Success successDiv.textContent = `Tag "${tagName}" created successfully!`; successDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Reload page after 2 seconds setTimeout(() => { window.location.reload(); }, 2000); } catch (error) { errorDiv.textContent = error.message; errorDiv.style.display = 'block'; successDiv.style.display = 'none'; submitBtn.disabled = false; submitBtn.textContent = 'Create Tag'; } }); // Focus on tag name input document.getElementById('tag-name').focus(); } // Function to add the "Create Tag" button to the tags page function addCreateTagButton() { const repoInfo = getRepoInfo(); if (!repoInfo) { console.log('[GitHub Tag Creator] No repo info found'); return; } // Don't add multiple buttons if (document.querySelector('.tag-creator-button')) { console.log('[GitHub Tag Creator] Button already exists'); return; } console.log('[GitHub Tag Creator] Looking for places to add button...'); // Try multiple selectors to find the right place const selectors = [ 'nav[aria-label="Releases and Tags"]', '.subnav', '.repository-content nav', '.BorderGrid-row nav', // Look for the nav containing the Tags link 'a[href*="/tags"]' ]; let targetElement = null; let placement = null; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { console.log(`[GitHub Tag Creator] Found element with selector: ${selector}`, element); // If we found the Tags link, get its parent nav if (selector.includes('href*="/tags"')) { targetElement = element.closest('nav'); placement = 'after-nav'; } else { targetElement = element; placement = 'after-nav'; } break; } } if (!targetElement) { console.log('[GitHub Tag Creator] No suitable location found for button'); return; } console.log('[GitHub Tag Creator] Adding button to:', targetElement); const button = document.createElement('button'); button.className = 'tag-creator-inline-button tag-creator-button'; button.textContent = 'Create Tag'; button.onclick = () => showTagCreatorModal(repoInfo); // Insert the button if (placement === 'after-nav' && targetElement.tagName === 'NAV') { // Create a wrapper div to hold the button const wrapper = document.createElement('div'); wrapper.style.display = 'inline-block'; wrapper.style.verticalAlign = 'middle'; wrapper.appendChild(button); // Insert after the nav targetElement.parentNode.insertBefore(wrapper, targetElement.nextSibling); } else { // Fallback: just append to the element targetElement.appendChild(button); } console.log('[GitHub Tag Creator] Button added successfully'); } // Wait for the page to be ready function waitForPageReady() { // Check if we're on a tags page if (!window.location.pathname.includes('/tags')) { console.log('[GitHub Tag Creator] Not on tags page, skipping'); return; } // Try to add button immediately addCreateTagButton(); // Also try after a delay setTimeout(() => { addCreateTagButton(); }, 1000); // And watch for dynamic changes const observer = new MutationObserver((mutations) => { // Only react to significant changes for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { // Check if nav elements were added for (const node of mutation.addedNodes) { if (node.nodeType === 1 && (node.tagName === 'NAV' || node.querySelector?.('nav'))) { console.log('[GitHub Tag Creator] Nav element added, trying to add button'); setTimeout(addCreateTagButton, 100); break; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', waitForPageReady); } else { waitForPageReady(); } })();