// ==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 = `

Create New Tag

Need a token? Create one here with 'repo' scope.
`; // 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(); } })();