base MR commit replication

This commit is contained in:
Laura Liberda 2021-02-20 00:07:00 +01:00
parent abcffc8b7a
commit 13de9bd4aa
4 changed files with 157 additions and 10 deletions

View file

@ -48,6 +48,7 @@
"@oclif/config": "^1.17.0",
"@oclif/plugin-help": "^3.2.1",
"appdata-path": "^1.0.0",
"email-addresses": "^4.0.0",
"fs-extra": "^9.1.0",
"got": "^11.8.1",
"graphql": "^15.5.0",
@ -59,6 +60,7 @@
"@graphql-codegen/introspection": "^1.18.1",
"@graphql-codegen/typescript": "^1.20.2",
"@oclif/dev-cli": "^1.26.0",
"@types/email-addresses": "^3.0.0",
"@types/fs-extra": "^9.0.6",
"@types/iarna__toml": "^2.0.1",
"@types/inquirer": "^7.3.1",

View file

@ -12,6 +12,14 @@ export default class Replicate extends Command {
source: flags.string({ char: 's', required: true }),
dest: flags.string({ char: 'd', required: true }),
destBranch: flags.string({
description: 'name of the new branch (optional for MRs, required for commits)',
}),
doNotCommit: flags.boolean({
description: 'do not commit the applied changes (for MRs)',
allowNo: false,
}),
};
static args = [];
@ -19,22 +27,54 @@ export default class Replicate extends Command {
async run() {
const { flags } = this.parse(Replicate);
const { destBranch, doNotCommit } = flags;
const sourcePath = parsePath(flags.source);
const destPath = parsePath(flags.dest);
assert(sourcePath.entity === ENTITY_TYPE.ISSUE, 'Only issues are supported now');
assert(sourcePath.entityID, 'Source must be a repo element, not a repo itself');
const cc = new Copykitku();
await cc.initialize();
const sourceVendor = cc.vendorManagers.find((v) => v.vendor.domain === sourcePath.domain);
const kitku = new Copykitku();
await kitku.initialize();
const sourceVendor = kitku.vendorManagers.find((v) => v.vendor.domain === sourcePath.domain);
assert(sourceVendor, 'Source vendor not found in config');
const destVendor = cc.vendorManagers.find((v) => v.vendor.domain === destPath.domain);
const destVendor = kitku.vendorManagers.find((v) => v.vendor.domain === destPath.domain);
assert(destVendor, 'Destination vendor not found in config');
const sourceRepo = await sourceVendor.getRepo(sourcePath.path);
const destRepo = await destVendor.getRepo(destPath.path);
const sourceEntity = await sourceRepo.getIssue(sourcePath.entityID);
sourceEntity.content += `\n\nReplicated from ${sourceEntity.url} with [Copykitku](https://git.sakamoto.pl/laudompat/copykitku)`;
const replicatedEntity = await destRepo.replicateIssue(sourceEntity);
console.log(`Replicated successfully: ${replicatedEntity.url}`);
switch (sourcePath.entity) {
case ENTITY_TYPE.ISSUE: {
const sourceIssue = await sourceRepo.getIssue(sourcePath.entityID);
const repl = await kitku.replicateIssue(sourceIssue, destRepo);
console.log(`Replicated successfully: ${repl.url}`);
break;
}
case ENTITY_TYPE.MERGE_REQUEST: {
const sourceMR = await sourceRepo.getMergeRequest(sourcePath.entityID);
const repl = await kitku.replicateMergeRequest(sourceMR, destRepo, {
destBranch,
doNotCommit,
});
// Copykitku.replicateMergeRequest() does not replicate MRs fully yet, it just applies the commits.
// uncomment the line below once the replication is done and the method returns a MergeRequest object
// console.log(`Replicated successfully: ${repl.url}`);
break;
}
case ENTITY_TYPE.COMMIT: {
// there's no way to get a single commit from repository yet
/*
const sourceCommit = await sourceRepo.getCommit(sourcePath.entityID);
const repl = await kitku.replicateCommits(sourceCommit, destRepo, {
destBranch,
doNotCommit,
});
*/
console.log('No commit replication yet, sorry');
break;
}
default: {
throw new Error('Unknown entity type');
}
}
}
}

View file

@ -7,7 +7,22 @@
*/
import path from 'path';
import { VendorManager, CopykitkuConfig, CopykitkuProfile } from './types';
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 {
@ -30,4 +45,82 @@ export default class Copykitku {
);
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,
});
}
}
}
}

View file

@ -1184,6 +1184,13 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/email-addresses@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/email-addresses/-/email-addresses-3.0.0.tgz#c8c1ab4606ea7b320205faefbd65aea28ac0035c"
integrity sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==
dependencies:
email-addresses "*"
"@types/fs-extra@^9.0.6":
version "9.0.6"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.6.tgz#488e56b77299899a608b8269719c1d133027a6ab"
@ -2322,6 +2329,11 @@ elegant-spinner@^1.0.1:
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
email-addresses@*, email-addresses@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-4.0.0.tgz#94fa214c30f943b02eaf91da717d89ff6a19e345"
integrity sha512-Nas3sSSiD5lSIoqBos0FMjB9h4clHxXuAahHKGJ5doRWavEB7pBHzOxnI7R5f1MuGNrrSnsZFJ81HCBv0DZmnw==
"emoji-regex@>=6.0.0 <=6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"