您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Codeberg extensions. Also works for other forgejo and gitea hosts.
// ==UserScript== // @name berx // @namespace Taywee // @description Codeberg extensions. Also works for other forgejo and gitea hosts. // @match https://codeberg.org/* // @version 1.4.7 // @author Taylor C. Richberger // @homepageURL https://codeberg.org/Taywee/berx // @license MPL-2.0 // @grant GM.setClipboard // ==/UserScript== (function () { 'use strict'; const settings_path = '/user/settings/applications'; const storage_key = 'berx-access-key'; function auth_header() { const key = localStorage.getItem(storage_key); if (!key) { return null; } return `token ${key}`; } function setup_token_settings() { const document_fragment = document.createDocumentFragment(); if (localStorage.getItem(storage_key) === null) { const form = document_fragment.appendChild(document.createElement('form')); form.classList.add('ui', 'form', 'ignore-dirty'); const field = form.appendChild(document.createElement('div')); field.classList.add('field'); const label = field.appendChild(document.createElement('label')); label.setAttribute('for', 'berx-token'); label.textContent = 'Token'; const input = field.appendChild(document.createElement('input')); input.id = 'berx-token'; input.name = 'berx-token'; const flash_key = document.querySelector('.flash-info.flash-message p')?.textContent; if (flash_key != null) { input.value = flash_key; } const button = form.appendChild(document.createElement('button')); button.classList.add('button', 'ui', 'green'); button.textContent = 'Submit'; button.type = 'button'; button.addEventListener('click', () => { localStorage.setItem(storage_key, input.value); setup_token_settings(); }); } else { const right_float = document_fragment.appendChild(document.createElement('div')); right_float.classList.add('right', 'floated', 'content'); const button = right_float.appendChild(document.createElement('button')); button.type = 'button'; button.classList.add('ui', 'red', 'tiny', 'button', 'delete-button'); button.textContent = 'Delete'; const p = document_fragment.appendChild(document.createElement('p')); p.textContent = 'An Access Token is set.'; button.addEventListener('click', () => { localStorage.removeItem(storage_key); setup_token_settings(); }); } const token_item = document.getElementById('berx-token-item'); token_item?.replaceChildren(document_fragment); } if (window.location.pathname === settings_path) { const user_setting_content = document.querySelector('.user-setting-content'); const header = document.createElement('h4'); header.classList.add('ui', 'top', 'attached', 'header'); header.textContent = 'berx Access Token'; const body = document.createElement('div'); body.classList.add('ui', 'attached', 'segment', 'bottom'); const key_list = body.appendChild(document.createElement('div')); key_list.classList.add('ui', 'key', 'list'); const description = key_list.appendChild(document.createElement('div')); description.classList.add('item'); description.textContent = 'To function, berx needs an Access Token with write:issue and write:repository.'; const token_item = key_list.appendChild(document.createElement('div')); token_item.classList.add('item'); token_item.id = 'berx-token-item'; const document_fragment = document.createDocumentFragment(); document_fragment.appendChild(header); document_fragment.appendChild(body); user_setting_content?.children[0].before(document_fragment); setup_token_settings(); } const issue_regex = /^\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<index>\d+)$/; const illegal = /[^A-Za-z0-9-]+/g; const refs_heads = /^refs\/heads\//; const trailing_hyphens = /-+$/; const path = window.location.pathname; /// If the value is undefined, return an empty array, otherwise return an array /// with the single element. function filter(value) { if (value === undefined) { return []; } else { return [value]; } } async function setup_issue_pr(groups, output) { const info = message => { output.appendChild(document.createElement('li')).textContent = message; }; const key = auth_header(); if (!key) { info(`API key is not set, redirecting to settings.`); window.location.href = '/user/settings/applications'; return; } const authorization = key; async function request(path, extras = {}) { const response = await fetch(path, { headers: { Authorization: authorization, 'Content-Type': 'application/json', Accept: 'application/json' }, ...extras }); if (!response.ok) { throw new Error(`Fetch response had error: ${response.status} ${response.statusText}`); } return await response.json(); } function get(path) { return request(path); } function post(path, body) { return request(path, { method: 'POST', body: JSON.stringify(body) }); } function patch(path, body) { return request(path, { method: 'PATCH', body: JSON.stringify(body) }); } const api_base = '/api/v1'; function getIssue(owner, repo, index) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return get(`${api_base}/repos/${owner}/${repo}/issues/${index}`); } function getRepo(owner, repo) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return get(`${api_base}/repos/${owner}/${repo}`); } function getBranch(owner, repo, branch) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); branch = encodeURIComponent(branch); return get(`${api_base}/repos/${owner}/${repo}/branches/${branch}`); } function getPullRequests(owner, repo) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return get(`${api_base}/repos/${owner}/${repo}/pulls?state=open`); } function createBranch(owner, repo, options) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return post(`${api_base}/repos/${owner}/${repo}/branches`, options); } function createPullRequest(owner, repo, options) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return post(`${api_base}/repos/${owner}/${repo}/pulls`, options); } function editIssue(owner, repo, index, options) { owner = encodeURIComponent(owner); repo = encodeURIComponent(repo); return patch(`${api_base}/repos/${owner}/${repo}/issues/${index}`, options); } const owner = groups.owner; const repo = groups.repo; const index = parseInt(groups.index, 10); const [issue, repository] = await Promise.all([getIssue(owner, repo, index), getRepo(owner, repo)]); let branch; if (issue.ref) { branch = await getBranch(owner, repo, issue.ref.replace(refs_heads, '')); } else { const branch_title = issue?.title?.toLowerCase()?.replaceAll(illegal, '-')?.substring(0, 128)?.replace(trailing_hyphens, ''); const branch_name = branch_title ? `issues/${index}-${branch_title}` : `issues/${index}`; try { info(`Trying to find branch by name ${branch_name}`); branch = await getBranch(owner, repo, branch_name); } catch (_) { branch = await createBranch(owner, repo, { new_branch_name: branch_name, old_ref_name: `heads/${repository.default_branch}` }); } info(`Assigning branch ${branch_name} to issue`); await editIssue(owner, repo, index, { ref: branch_name }); } info(`Finding open pull request for branch`); const pull_requests = await getPullRequests(owner, repo); let pull_request = pull_requests.find(each => each?.head?.ref === branch.name); if (pull_request == null) { info(`Creating pull request for branch`); pull_request = await createPullRequest(owner, repo, { assignees: issue.assignees?.map(user => user?.login)?.flatMap(filter), base: repository.default_branch, body: issue.body ? `${issue.body}\n\ncloses #${issue.number}` : `closes #${issue.number}`, due_date: issue.due_date, head: branch.name, labels: issue.labels?.map(label => label.id).flatMap(filter), milestone: issue.milestone?.id, title: issue.title ? `WIP: ${issue.title}` : undefined }); } info(`Copying text to clipboard`); GM.setClipboard(`git fetch origin; git switch ${branch.name}`); const pr = `/${owner}/${repo}/pulls/${pull_request.number}`; info(`Redirecting to ${pr}`); window.location.href = pr; } const match = issue_regex.exec(path); if (match !== null) { const groups = match.groups; const fragment = document.createDocumentFragment(); const button = fragment.appendChild(document.createElement('button')); const output = fragment.appendChild(document.createElement('ul')); const info = message => { output.appendChild(document.createElement('li')).textContent = message; }; button.classList.add('ui', 'green', 'icon', 'button'); button.textContent = 'Add branch and PR'; const select_branch = document.querySelector('.select-branch'); select_branch?.after(fragment); button.addEventListener('click', async () => { try { await setup_issue_pr(groups, output); } catch (error) { info(`Error: ${error}. Trying one more time.`); try { await setup_issue_pr(groups, output); } catch (error) { info(`Error: ${error}. Trying again will work quite often.`); } } }); } })();