Files
Lauri Ojansivu 935e68dfdd GitHub organization import: repositories, issues, releases, Actions, wiki.
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 !
2026-10-14 12:43:36 +03:00

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,
};