/* * Copykitku. Copyright (C) 2020 selfisekai 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 . */ 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; 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, remote, targetBranch, patchHook, includePaths, excludePaths, } = opts; await this.replicateCommits(sourceMR.commits, destination, { destBranch, doNotCommit, 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; patchHook?: string | null; includePaths?: string[]; excludePaths?: string[]; }, ) { const { destBranch, doNotCommit, 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) : 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, }); } } } }