berx

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

目前为 2023-09-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name berx
  3. // @namespace Taywee
  4. // @description Codeberg extensions. Also works for other forgejo and gitea hosts.
  5. // @match https://codeberg.org/*
  6. // @version 1.4.4
  7. // @author Taylor C. Richberger
  8. // @homepageURL https://codeberg.org/Taywee/berx
  9. // @license MPL-2.0
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. const settings_path = '/user/settings/applications';
  16. const storage_key = 'berx-access-key';
  17. function auth_header() {
  18. const key = localStorage.getItem(storage_key);
  19. if (!key) {
  20. return null;
  21. }
  22. return `token ${key}`;
  23. }
  24. function setup_token_settings() {
  25. const document_fragment = document.createDocumentFragment();
  26. if (localStorage.getItem(storage_key) === null) {
  27. const form = document_fragment.appendChild(document.createElement('form'));
  28. form.classList.add('ui', 'form', 'ignore-dirty');
  29. const field = form.appendChild(document.createElement('div'));
  30. field.classList.add('field');
  31. const label = field.appendChild(document.createElement('label'));
  32. label.setAttribute('for', 'berx-token');
  33. label.textContent = 'Token';
  34. const input = field.appendChild(document.createElement('input'));
  35. input.id = 'berx-token';
  36. input.name = 'berx-token';
  37. const flash_key = document.querySelector('.flash-info.flash-message p')?.textContent;
  38. if (flash_key != null) {
  39. input.value = flash_key;
  40. }
  41. const button = form.appendChild(document.createElement('button'));
  42. button.classList.add('button', 'ui', 'green');
  43. button.textContent = 'Submit';
  44. button.type = 'button';
  45. button.addEventListener('click', () => {
  46. localStorage.setItem(storage_key, input.value);
  47. setup_token_settings();
  48. });
  49. } else {
  50. const right_float = document_fragment.appendChild(document.createElement('div'));
  51. right_float.classList.add('right', 'floated', 'content');
  52. const button = right_float.appendChild(document.createElement('button'));
  53. button.type = 'button';
  54. button.classList.add('ui', 'red', 'tiny', 'button', 'delete-button');
  55. button.textContent = 'Delete';
  56. const p = document_fragment.appendChild(document.createElement('p'));
  57. p.textContent = 'An Access Token is set.';
  58. button.addEventListener('click', () => {
  59. localStorage.removeItem(storage_key);
  60. setup_token_settings();
  61. });
  62. }
  63. const token_item = document.getElementById('berx-token-item');
  64. token_item?.replaceChildren(document_fragment);
  65. }
  66. if (window.location.pathname === settings_path) {
  67. const user_setting_content = document.querySelector('.user-setting-content');
  68. const header = document.createElement('h4');
  69. header.classList.add('ui', 'top', 'attached', 'header');
  70. header.textContent = 'berx Access Token';
  71. const body = document.createElement('div');
  72. body.classList.add('ui', 'attached', 'segment', 'bottom');
  73. const key_list = body.appendChild(document.createElement('div'));
  74. key_list.classList.add('ui', 'key', 'list');
  75. const description = key_list.appendChild(document.createElement('div'));
  76. description.classList.add('item');
  77. description.textContent = 'To function, berx needs an Access Token with write:issue and write:repository.';
  78. const token_item = key_list.appendChild(document.createElement('div'));
  79. token_item.classList.add('item');
  80. token_item.id = 'berx-token-item';
  81. const document_fragment = document.createDocumentFragment();
  82. document_fragment.appendChild(header);
  83. document_fragment.appendChild(body);
  84. user_setting_content?.children[0].before(document_fragment);
  85. setup_token_settings();
  86. }
  87.  
  88. const issue_regex = /^\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<index>\d+)$/;
  89. const illegal = /[^A-Za-z0-9-]+/g;
  90. const refs_heads = /^refs\/heads\//;
  91. const trailing_hyphens = /-+$/;
  92. const path = window.location.pathname;
  93.  
  94. /// If the value is undefined, return an empty array, otherwise return an array
  95. /// with the single element.
  96. function filter(value) {
  97. if (value === undefined) {
  98. return [];
  99. } else {
  100. return [value];
  101. }
  102. }
  103. async function setup_issue_pr(groups, output) {
  104. const info = message => {
  105. output.appendChild(document.createElement('li')).textContent = message;
  106. };
  107. const key = auth_header();
  108. if (!key) {
  109. info(`API key is not set, redirecting to settings.`);
  110. window.location.href = '/user/settings/applications';
  111. return;
  112. }
  113. const authorization = key;
  114. async function request(path, extras = {}) {
  115. const response = await fetch(path, {
  116. headers: {
  117. Authorization: authorization,
  118. 'Content-Type': 'application/json',
  119. Accept: 'application/json'
  120. },
  121. ...extras
  122. });
  123. if (!response.ok) {
  124. throw new Error(`Fetch response had error: ${response.status} ${response.statusText}`);
  125. }
  126. return await response.json();
  127. }
  128. function get(path) {
  129. return request(path);
  130. }
  131. function post(path, body) {
  132. return request(path, {
  133. method: 'POST',
  134. body: JSON.stringify(body)
  135. });
  136. }
  137. function patch(path, body) {
  138. return request(path, {
  139. method: 'PATCH',
  140. body: JSON.stringify(body)
  141. });
  142. }
  143. const api_base = '/api/v1';
  144. function getIssue(owner, repo, index) {
  145. owner = encodeURIComponent(owner);
  146. repo = encodeURIComponent(repo);
  147. return get(`${api_base}/repos/${owner}/${repo}/issues/${index}`);
  148. }
  149. function getRepo(owner, repo) {
  150. owner = encodeURIComponent(owner);
  151. repo = encodeURIComponent(repo);
  152. return get(`${api_base}/repos/${owner}/${repo}`);
  153. }
  154. function getBranch(owner, repo, branch) {
  155. owner = encodeURIComponent(owner);
  156. repo = encodeURIComponent(repo);
  157. branch = encodeURIComponent(branch);
  158. return get(`${api_base}/repos/${owner}/${repo}/branches/${branch}`);
  159. }
  160. function getPullRequests(owner, repo) {
  161. owner = encodeURIComponent(owner);
  162. repo = encodeURIComponent(repo);
  163. return get(`${api_base}/repos/${owner}/${repo}/pulls?state=open`);
  164. }
  165. function createBranch(owner, repo, options) {
  166. owner = encodeURIComponent(owner);
  167. repo = encodeURIComponent(repo);
  168. return post(`${api_base}/repos/${owner}/${repo}/branches`, options);
  169. }
  170. function createPullRequest(owner, repo, options) {
  171. owner = encodeURIComponent(owner);
  172. repo = encodeURIComponent(repo);
  173. return post(`${api_base}/repos/${owner}/${repo}/pulls`, options);
  174. }
  175. function editIssue(owner, repo, index, options) {
  176. owner = encodeURIComponent(owner);
  177. repo = encodeURIComponent(repo);
  178. return patch(`${api_base}/repos/${owner}/${repo}/issues/${index}`, options);
  179. }
  180. const owner = groups.owner;
  181. const repo = groups.repo;
  182. const index = parseInt(groups.index, 10);
  183. const [issue, repository] = await Promise.all([getIssue(owner, repo, index), getRepo(owner, repo)]);
  184. let branch;
  185. if (issue.ref) {
  186. branch = await getBranch(owner, repo, issue.ref.replace(refs_heads, ''));
  187. } else {
  188. const branch_title = issue?.title?.toLowerCase()?.replaceAll(illegal, '-')?.substring(0, 128)?.replace(trailing_hyphens, '');
  189. const branch_name = branch_title ? `issues/${index}-${branch_title}` : `issues/${index}`;
  190. try {
  191. info(`Trying to find branch by name ${branch_name}`);
  192. branch = await getBranch(owner, repo, branch_name);
  193. } catch (_) {
  194. branch = await createBranch(owner, repo, {
  195. new_branch_name: branch_name,
  196. old_ref_name: `heads/${repository.default_branch}`
  197. });
  198. }
  199. info(`Assigning branch ${branch_name} to issue`);
  200. await editIssue(owner, repo, index, {
  201. ref: branch_name
  202. });
  203. }
  204. info(`Finding open pull request for branch`);
  205. const pull_requests = await getPullRequests(owner, repo);
  206. let pull_request = pull_requests.find(each => each?.head?.ref === branch.name);
  207. if (pull_request == null) {
  208. info(`Creating pull request for branch`);
  209. pull_request = await createPullRequest(owner, repo, {
  210. assignees: issue.assignees?.map(user => user?.login)?.flatMap(filter),
  211. base: repository.default_branch,
  212. body: issue.body ? `${issue.body}\n\ncloses #${issue.number}` : `closes #${issue.number}`,
  213. due_date: issue.due_date,
  214. head: branch.name,
  215. labels: issue.labels?.map(label => label.id).flatMap(filter),
  216. milestone: issue.milestone?.id,
  217. title: issue.title ? `WIP: ${issue.title}` : undefined
  218. });
  219. }
  220. info(`Copying text to clipboard`);
  221. await navigator.clipboard.writeText(`git fetch origin; git switch ${branch.name}`);
  222. const pr = `/${owner}/${repo}/pulls/${pull_request.number}`;
  223. info(`Redirecting to ${pr}`);
  224. window.location.href = pr;
  225. }
  226. const match = issue_regex.exec(path);
  227. if (match !== null) {
  228. const groups = match.groups;
  229. const fragment = document.createDocumentFragment();
  230. const button = fragment.appendChild(document.createElement('button'));
  231. const output = fragment.appendChild(document.createElement('ul'));
  232. const info = message => {
  233. output.appendChild(document.createElement('li')).textContent = message;
  234. };
  235. button.classList.add('ui', 'green', 'icon', 'button');
  236. button.textContent = 'Add branch and PR';
  237. const select_branch = document.querySelector('.select-branch');
  238. select_branch?.after(fragment);
  239. button.addEventListener('click', async () => {
  240. try {
  241. await setup_issue_pr(groups, output);
  242. } catch (error) {
  243. info(`Error: ${error}. Trying one more time.`);
  244. try {
  245. await setup_issue_pr(groups, output);
  246. } catch (error) {
  247. info(`Error: ${error}. Trying again will work quite often.`);
  248. }
  249. }
  250. });
  251. }
  252.  
  253. })();