berx

Codeberg extensions. Also works for other forgejo and gitea hosts.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();