copykitku/src/copykitku.ts
2021-02-26 14:01:58 +01:00

202 lines
6.1 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,
Commit,
} 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;
doNotPush?: boolean | null;
mergeToMainBranch?: boolean | null;
patchHook?: string | null;
remote: string;
targetBranch: string;
includePaths?: string[];
excludePaths?: string[];
},
) {
const destBranch =
opts.destBranch || `${sourceMR.repo.owner.username}/${sourceMR.repo.name}/mr-${sourceMR.id}`;
const {
doNotCommit,
doNotPush,
mergeToMainBranch,
remote,
targetBranch,
patchHook,
includePaths,
excludePaths,
} = opts;
await this.replicateCommits(sourceMR.commits, destination, {
destBranch,
doNotCommit,
mergeToMainBranch,
patchHook,
includePaths,
excludePaths,
});
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;
}
public async replicateCommits(
sourceCommit: Commit | Commit[],
destination: RepoManager,
opts: {
destBranch: string;
doNotCommit?: boolean | null;
mergeToMainBranch?: boolean | null;
patchHook?: string | null;
includePaths?: string[];
excludePaths?: string[];
},
) {
const {
destBranch,
doNotCommit,
mergeToMainBranch,
patchHook,
includePaths,
excludePaths,
} = opts;
const commits = Array.isArray(sourceCommit) ? sourceCommit : [sourceCommit];
const patchHookCall = patchHook
? (require(path.join(process.cwd(), ...patchHook.split('/'))) as (
patchContent: string,
) => string | Promise<string>)
: null;
// 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;
let content = await commit.patchContent();
if (patchHookCall) {
content = await patchHookCall(content);
}
await fs.writeFile(filename, content);
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]),
),
);
if (doNotCommit !== true) {
await git.commit(patch.title + (patch.content ? '\n\n' + patch.content : ''), {
'--author': patch.from,
});
}
}
if (doNotCommit !== true && mergeToMainBranch === true) {
// TODO: check the main branch name (T32)
await git.checkout('master');
await git.merge([destBranch]);
}
return true;
}
}