mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
Import -> GitHub -> Import a whole GitHub organization makes every repository of an organization or user its own board, opening in Board View / Version Control. Issues go through the ordinary importBoard, as the person who started the job, with their comments, labels and files. Releases are cards in a Releases list, their files attached only when chosen. Each .github/workflows file becomes Rules. The wiki is read from its git repository with isomorphic-git (1.42.4, pinned), every request through the SSRF guard and only to github.com: a page is a card in the Wiki list, its [[links]] point at and are attached to the linked pages' cards, and each commit that changed it is a dated edit in the card's history, so Restore brings back an older version. The job shows progress, can be cancelled, pauses on a restart and stops at a rate limit; the token is kept in memory only. Version Control gains Releases and Wiki tabs: the wiki as GitHub shows it, with the page list, _Sidebar and _Footer, links that stay in the tab, and each page's history with Restore. GitHub Actions workflows map to Rules: issue events to triggers, label, close, reopen, comment, assign and create steps to actions, actions/stale to an aging rule. Code - builds, tests, scripts - is never run: such a workflow is a disabled rule with its YAML as the description. WeKan's Rules gain what they lacked for it: a "commented on" trigger, an "add comment" action written as the person whose change ran the rule and only where they may comment, cron schedules in UTC, and a rule description shown on the rule. A disabled rule no longer fires on a schedule or from its button, which it did. The new strings are translated in every locale. Thanks to xet7 !
138 lines
5.4 KiB
JavaScript
138 lines
5.4 KiB
JavaScript
'use strict';
|
|
|
|
// A GitHub wiki as cards, for the GitHub organization import and Board View /
|
|
// Version Control's Wiki tab. A GitHub wiki is a git repository of Markdown
|
|
// files (<repo>.wiki.git): one file is one page, its file name is the page
|
|
// name with spaces as hyphens, `Home` is the start page, and `_Sidebar` and
|
|
// `_Footer` are drawn around every page.
|
|
//
|
|
// one page one card in the board's "Wiki" list
|
|
// page title the card title, on the minicard
|
|
// page text the card's description, in the opened card
|
|
// [[links]] "link to card": the linked page's card is attached
|
|
// to the card, and the link in the text points at it
|
|
// page history the card's history: each commit that changed the
|
|
// file is an edit of the description, so History
|
|
// shows it and Restore brings back an older text
|
|
//
|
|
// Pure, no Meteor and no git: the server reads the repository
|
|
// (server/lib/githubWiki.js) and hands this the files and their revisions.
|
|
// tests/githubWiki.test.cjs.
|
|
|
|
const WIKI_LIST_TITLE = 'Wiki';
|
|
const PAGE_EXTENSIONS = /\.(md|markdown|mdown|mkdn|textile|rdoc|org|creole|mediawiki|wiki|rst|asciidoc|adoc|pod|txt)$/i;
|
|
const SPECIAL_PAGES = ['_Sidebar', '_Footer'];
|
|
|
|
/** The page name of a wiki file, or null when the file is not a page. */
|
|
function pageNameFromPath(file) {
|
|
const name = String(file || '').split('/').pop();
|
|
if (!PAGE_EXTENSIONS.test(name)) return null;
|
|
return name.replace(PAGE_EXTENSIONS, '').replace(/-/g, ' ').trim() || null;
|
|
}
|
|
|
|
/** The key GitHub resolves a page by: case-insensitive, spaces and hyphens alike. */
|
|
function pageKey(name) {
|
|
return String(name || '').trim().replace(/[\s-]+/g, '-').replace(/\.(md|markdown)$/i, '').toLowerCase();
|
|
}
|
|
|
|
function isSpecialPage(name) {
|
|
return SPECIAL_PAGES.map(pageKey).includes(pageKey(name));
|
|
}
|
|
|
|
/**
|
|
* Every wiki link of a page's text, as { name, text, start, end, kind }:
|
|
* [[Page]] [[Text|Page]] [[Page|Text]] (GitHub accepts both orders)
|
|
* [text](Page-Name) [text](https://github.com/<o>/<r>/wiki/Page-Name)
|
|
* `isPage(key)` decides which [[a|b]] half is the page.
|
|
*/
|
|
function wikiLinks(text, { repoWikiUrl = '', isPage = () => false } = {}) {
|
|
const out = [];
|
|
const body = String(text || '');
|
|
const double = /\[\[([^\]|\n]+)(?:\|([^\]\n]+))?\]\]/g;
|
|
let m;
|
|
while ((m = double.exec(body))) {
|
|
let name = m[1].trim();
|
|
let label = name;
|
|
if (m[2]) {
|
|
const a = m[1].trim();
|
|
const b = m[2].trim();
|
|
// GitHub reads [[Link Text|Page]]; MediaWiki habit is [[Page|Text]].
|
|
[label, name] = isPage(pageKey(b)) || !isPage(pageKey(a)) ? [a, b] : [b, a];
|
|
}
|
|
out.push({ name, text: label, start: m.index, end: m.index + m[0].length, kind: 'wiki' });
|
|
}
|
|
const base = String(repoWikiUrl || '').replace(/\/+$/, '');
|
|
const md = /(?<!!)\[([^\]\n]*)\]\(([^)\s]+)\)/g;
|
|
while ((m = md.exec(body))) {
|
|
let target = m[2];
|
|
if (base && target.toLowerCase().startsWith(`${base.toLowerCase()}/`)) target = target.slice(base.length + 1);
|
|
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('/') || target.startsWith('#')) continue;
|
|
const name = decodeURIComponent(target.split('#')[0]);
|
|
if (!name || /\.(png|jpe?g|gif|svg|pdf|zip)$/i.test(name)) continue;
|
|
out.push({ name: name.replace(/-/g, ' '), text: m[1], start: m.index, end: m.index + m[0].length, kind: 'markdown' });
|
|
}
|
|
return out.sort((a, b) => a.start - b.start);
|
|
}
|
|
|
|
/**
|
|
* The text with each link to a page that became a card pointing at that
|
|
* card, and the ids of those cards, for "link to card".
|
|
* cardFor(key) -> { _id, url } | null
|
|
*/
|
|
function rewriteWikiLinks(text, { repoWikiUrl, cardFor }) {
|
|
const body = String(text || '');
|
|
const isPage = key => Boolean(cardFor(key));
|
|
const links = wikiLinks(body, { repoWikiUrl, isPage });
|
|
let out = '';
|
|
let at = 0;
|
|
const linked = [];
|
|
for (const link of links) {
|
|
if (link.start < at) continue;
|
|
const card = cardFor(pageKey(link.name));
|
|
if (!card) continue;
|
|
out += body.slice(at, link.start) + `[${link.text.replace(/[[\]]/g, '')}](${card.url})`;
|
|
at = link.end;
|
|
if (!linked.includes(card._id)) linked.push(card._id);
|
|
}
|
|
return { text: out + body.slice(at), linkedCardIds: linked };
|
|
}
|
|
|
|
/**
|
|
* The history of one page, oldest first, as the edits of its card's
|
|
* description: [{ previous, next, at, author, email, message, commit }].
|
|
* revisions: [{ commit, at, author, email, message, text }] oldest first,
|
|
* text null when the commit deleted the file.
|
|
*/
|
|
function pageHistory(revisions) {
|
|
const out = [];
|
|
let previous = null;
|
|
for (const r of Array.isArray(revisions) ? revisions : []) {
|
|
if (!r) continue;
|
|
const next = r.text == null ? null : String(r.text);
|
|
if (next === previous) continue;
|
|
out.push({ previous, next, at: r.at, author: r.author || '', email: r.email || '', message: r.message || '', commit: r.commit || '' });
|
|
previous = next;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The pages of a wiki, Home first, then by name - the order of GitHub's page list. */
|
|
function sortPages(pages) {
|
|
return [...pages].sort((a, b) => {
|
|
const home = p => (pageKey(p.name) === 'home' ? 0 : 1);
|
|
return home(a) - home(b) || a.name.localeCompare(b.name);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
WIKI_LIST_TITLE,
|
|
SPECIAL_PAGES,
|
|
pageNameFromPath,
|
|
pageKey,
|
|
isSpecialPage,
|
|
wikiLinks,
|
|
rewriteWikiLinks,
|
|
pageHistory,
|
|
sortPages,
|
|
};
|