copykitku/src/copykitku.ts
2021-02-20 00:07:00 +01:00

127 lines
4.2 KiB
TypeScript

/*
* Copykitku. Copyright (C) 2020 selfisekai <laura@selfisekai.rocks> and other contributors.
*
* This is free software, and you are welcome to redistribute it
* under the GNU General Public License 3.0 or later; see the LICENSE file for details,
* or, if the file is unavailable, visit <https://www.gnu.org/licenses/gpl-3.0-standalone.html>.
*/
import path from 'path';
import simpleGit from 'simple-git';
import addresses from 'email-addresses';
import fs from 'fs-extra';
import os from 'os';
import {
VendorManager,
CopykitkuConfig,
CopykitkuProfile,
Issue,
RepoManager,
VENDOR_TYPE,
MergeRequest,
RepoElement,
Commit,
ENTITY_TYPE,
} from './types';
import { getConfig, DEFAULT_CONFIG } from './utils';
export default class Copykitku {
vendorManagers: VendorManager[] = [];
config: CopykitkuConfig = DEFAULT_CONFIG;
public async initialize() {
this.config = getConfig();
this.vendorManagers = await Promise.all(
this.config.vendorConfigs
.map(
(profile) =>
[
require(path.join(__dirname, 'vendor', profile.vendor.type, 'vendormgr')).default,
profile,
] as [any, CopykitkuProfile],
)
.map(([VendorMgr, vendor]) => new VendorMgr(vendor.config) as VendorManager)
.map((VendorMgr) => VendorMgr.initialize()),
);
return this;
}
public async replicateIssue(sourceIssue: Issue, destination: RepoManager) {
// if markdown support on destination
if ([VENDOR_TYPE.GITHUB, VENDOR_TYPE.GITLAB].includes(destination.repo.vendor.type)) {
sourceIssue.content += `\n\nReplicated from ${sourceIssue.url} with [Copykitku](https://git.sakamoto.pl/laudompat/copykitku)`;
} else {
sourceIssue.content += `\n\nReplicated from ${sourceIssue.url} with https://git.sakamoto.pl/laudompat/copykitku`;
}
return destination.replicateIssue(sourceIssue);
}
public async replicateMergeRequest(
sourceMR: MergeRequest,
destination: RepoManager,
opts: { destBranch?: string; doNotCommit?: boolean | null } = {},
) {
const destBranch =
opts.destBranch || `${sourceMR.repo.owner.username}/${sourceMR.repo.name}/mr-${sourceMR.id}`;
const { doNotCommit } = opts;
await this.replicateCommits(sourceMR.commits, destination, { destBranch, doNotCommit });
// TODO: push commits to the repository and create the MR (with option to disable this behaviour)
}
public async replicateCommits(
sourceCommit: Commit | Commit[],
destination: RepoManager,
opts: { destBranch: string; doNotCommit?: boolean | null },
) {
const { destBranch, doNotCommit } = opts;
const commits = Array.isArray(sourceCommit) ? sourceCommit : [sourceCommit];
// saving patch files to /tmp or equivalent
const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), 'copykitku-'));
const patchFiles = await Promise.all(
commits.map(async (c, index) => {
const commit = c as Commit & {
patchFile: string;
from: string;
};
const filename = path.join(
tmpPath,
`${index.toString().padStart(4, '0')}-${commit.id}.patch`,
);
const content = await commit.patchContent();
await fs.writeFile(filename, content);
commit.patchFile = filename;
const [, from] = /^From: ([^\n]+)$/im.exec(content) || [, null];
const [, date] = /^Date: ([^\n]+)$/im.exec(content) || [, null];
if (!from || !date) {
throw new Error(`Commit ${commit.id} could not be parsed`);
}
if (!addresses.parseOneAddress(from)) {
throw new Error(`Commit ${commit.id} author could not be parsed`);
}
commit.from = from;
return commit;
}),
);
const git = simpleGit({ baseDir: process.cwd() });
// assuming that the repository in cwd is the desired destination
await git.checkoutLocalBranch(destBranch);
for (let i = 0; i < patchFiles.length; i += 1) {
const patch = patchFiles[i];
await git.applyPatch(patch.patchFile, ['--index']);
if (doNotCommit !== true) {
await git.commit(patch.title + (patch.content ? '\n\n' + patch.content : ''), {
'--author': patch.from,
});
}
}
}
}