copykitku/src/copykitku.ts

184 lines
5.7 KiB
TypeScript
Raw Normal View History

2020-08-21 11:53:55 +02:00
/*
2021-02-14 22:24:01 +01:00
* Copykitku. Copyright (C) 2020 selfisekai <laura@selfisekai.rocks> and other contributors.
2020-08-21 11:53:55 +02:00
*
* 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>.
*/
2020-08-05 23:33:36 +02:00
import path from 'path';
2021-02-20 00:07:00 +01:00
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,
Commit,
} from './types';
2020-08-05 23:33:36 +02:00
import { getConfig, DEFAULT_CONFIG } from './utils';
2021-02-14 22:24:01 +01:00
export default class Copykitku {
2020-08-05 23:33:36 +02:00
vendorManagers: VendorManager[] = [];
2021-02-14 22:24:01 +01:00
config: CopykitkuConfig = DEFAULT_CONFIG;
2020-08-05 23:33:36 +02:00
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,
2021-02-14 22:24:01 +01:00
] as [any, CopykitkuProfile],
2020-08-05 23:33:36 +02:00
)
.map(([VendorMgr, vendor]) => new VendorMgr(vendor.config) as VendorManager)
.map((VendorMgr) => VendorMgr.initialize()),
);
return this;
}
2021-02-20 00:07:00 +01:00
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,
2021-02-21 21:29:21 +01:00
opts: {
destBranch?: string;
doNotCommit?: boolean | null;
doNotPush?: boolean | null;
2021-02-24 01:33:32 +01:00
patchHook?: string | null;
2021-02-21 21:29:21 +01:00
remote: string;
targetBranch: string;
includePaths?: string[];
excludePaths?: string[];
2021-02-21 21:29:21 +01:00
},
2021-02-20 00:07:00 +01:00
) {
const destBranch =
opts.destBranch || `${sourceMR.repo.owner.username}/${sourceMR.repo.name}/mr-${sourceMR.id}`;
const {
doNotCommit,
doNotPush,
remote,
targetBranch,
patchHook,
includePaths,
excludePaths,
} = opts;
2021-02-20 00:07:00 +01:00
2021-02-24 01:33:32 +01:00
await this.replicateCommits(sourceMR.commits, destination, {
destBranch,
doNotCommit,
patchHook,
includePaths,
excludePaths,
2021-02-24 01:33:32 +01:00
});
2021-02-20 00:07:00 +01:00
2021-02-21 21:29:21 +01:00
if (doNotCommit !== true && doNotPush !== true) {
const git = simpleGit({ baseDir: process.cwd() });
git.push(remote, destBranch);
// if markdown support on destination
if ([VENDOR_TYPE.GITHUB, VENDOR_TYPE.GITLAB].includes(destination.repo.vendor.type)) {
sourceMR.content += `\n\nReplicated from ${sourceMR.url} with [Copykitku](https://git.sakamoto.pl/laudompat/copykitku)`;
} else {
sourceMR.content += `\n\nReplicated from ${sourceMR.url} with https://git.sakamoto.pl/laudompat/copykitku`;
}
return destination.replicateMergeRequest(sourceMR, destBranch, targetBranch);
}
return true;
2021-02-20 00:07:00 +01:00
}
public async replicateCommits(
sourceCommit: Commit | Commit[],
destination: RepoManager,
2021-02-24 01:33:32 +01:00
opts: {
destBranch: string;
doNotCommit?: boolean | null;
patchHook?: string | null;
includePaths?: string[];
excludePaths?: string[];
2021-02-24 01:33:32 +01:00
},
2021-02-20 00:07:00 +01:00
) {
const { destBranch, doNotCommit, patchHook, includePaths, excludePaths } = opts;
2021-02-20 00:07:00 +01:00
const commits = Array.isArray(sourceCommit) ? sourceCommit : [sourceCommit];
2021-02-24 01:33:32 +01:00
const patchHookCall = patchHook
? (require(path.join(process.cwd(), ...patchHook.split('/'))) as (
patchContent: string,
) => string | Promise<string>)
: null;
2021-02-20 00:07:00 +01:00
// 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`,
);
commit.patchFile = filename;
2021-02-24 01:33:32 +01:00
let content = await commit.patchContent();
if (patchHookCall) {
content = await patchHookCall(content);
}
await fs.writeFile(filename, content);
2021-02-20 00:07:00 +01:00
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'].concat(
...(includePaths || []).map((inc) => ['--include', inc]),
...(excludePaths || []).map((inc) => ['--exclude', inc]),
),
);
2021-02-20 00:07:00 +01:00
if (doNotCommit !== true) {
await git.commit(patch.title + (patch.content ? '\n\n' + patch.content : ''), {
'--author': patch.from,
});
}
}
}
2020-08-05 23:33:36 +02:00
}