- // ==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.`);
- }
- }
- });
- }
-
- })();