202 lines
6.1 KiB
TypeScript
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;
|
|
}
|
|
}
|