From 859f3caf396ec6138be76bca8a4b161ba1a74a39 Mon Sep 17 00:00:00 2001 From: Laura Liberda Date: Sun, 21 Feb 2021 21:29:21 +0100 Subject: [PATCH] full MR replication --- src/cli/replicate.ts | 22 ++++-- src/copykitku.ts | 27 +++++-- src/types.ts | 8 +++ src/vendor/github/repomgr.ts | 134 ++++++++++++++++++++++++----------- src/vendor/gitlab/repomgr.ts | 106 +++++++++++++++++++-------- 5 files changed, 220 insertions(+), 77 deletions(-) diff --git a/src/cli/replicate.ts b/src/cli/replicate.ts index 3f470b7..f3fb6ff 100644 --- a/src/cli/replicate.ts +++ b/src/cli/replicate.ts @@ -20,6 +20,14 @@ export default class Replicate extends Command { description: 'do not commit the applied changes (for MRs)', allowNo: false, }), + remote: flags.string({ + description: 'git remote where the commits are pushed (for MRs)', + default: 'origin', + }), + targetBranch: flags.string({ + description: 'branch to which the MR should target', + default: 'master', // TODO: check in the repository instead of taking a wild guess + }), }; static args = []; @@ -27,7 +35,7 @@ export default class Replicate extends Command { async run() { const { flags } = this.parse(Replicate); - const { destBranch, doNotCommit } = flags; + const { destBranch, doNotCommit, remote, targetBranch } = flags; const sourcePath = parsePath(flags.source); const destPath = parsePath(flags.dest); @@ -54,10 +62,16 @@ export default class Replicate extends Command { const repl = await kitku.replicateMergeRequest(sourceMR, destRepo, { destBranch, doNotCommit, + remote, + targetBranch, }); - // 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}`); + if (repl === true) { + // patches got applied to the branch, without pushing and creating a MR (due to --doNotCommit) + console.log('Replicated commits successfully'); + } else { + // commits got pushed and a MR was created + console.log(`Replicated successfully: ${repl.url}`); + } break; } case ENTITY_TYPE.COMMIT: { diff --git a/src/copykitku.ts b/src/copykitku.ts index 2d9ae6a..53ccdf0 100644 --- a/src/copykitku.ts +++ b/src/copykitku.ts @@ -19,9 +19,7 @@ import { RepoManager, VENDOR_TYPE, MergeRequest, - RepoElement, Commit, - ENTITY_TYPE, } from './types'; import { getConfig, DEFAULT_CONFIG } from './utils'; @@ -60,15 +58,34 @@ export default class Copykitku { public async replicateMergeRequest( sourceMR: MergeRequest, destination: RepoManager, - opts: { destBranch?: string; doNotCommit?: boolean | null } = {}, + opts: { + destBranch?: string; + doNotCommit?: boolean | null; + doNotPush?: boolean | null; + remote: string; + targetBranch: string; + }, ) { const destBranch = opts.destBranch || `${sourceMR.repo.owner.username}/${sourceMR.repo.name}/mr-${sourceMR.id}`; - const { doNotCommit } = opts; + const { doNotCommit, doNotPush, remote, targetBranch } = 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) + 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( diff --git a/src/types.ts b/src/types.ts index 1d50689..68d0078 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,14 @@ export interface RepoManager { getIssue: (id: string) => Promise; replicateIssue: (issue: Issue) => Promise; getMergeRequest: (id: string) => Promise; + /** this method is to be invoked after the commits are pushed to the branch */ + replicateMergeRequest: ( + mergeRequest: MergeRequest, + /** where the commits have been pushed */ + destBranch: string, + /** where the newly-created MR should target */ + targetBranch: string, + ) => Promise; } export enum ENTITY_TYPE { diff --git a/src/vendor/github/repomgr.ts b/src/vendor/github/repomgr.ts index 595f515..88ddf79 100644 --- a/src/vendor/github/repomgr.ts +++ b/src/vendor/github/repomgr.ts @@ -20,7 +20,13 @@ import { } from '../../types'; import GitHubVendorManager from './vendormgr'; import assert from 'assert'; -import { GHMutation, GHMutationCreateIssueArgs, GHPullRequestCommit } from './api-types'; +import { + GHMutation, + GHMutationCreateIssueArgs, + GHMutationCreatePullRequestArgs, + GHPullRequest, + GHPullRequestCommit, +} from './api-types'; export default class GitHubRepoManager implements RepoManager { vendorMgr: GitHubVendorManager; @@ -153,6 +159,48 @@ export default class GitHubRepoManager implements RepoManager { }; } + protected async _parsePR(pullRequest: GHPullRequest): Promise { + assert(pullRequest.commits.nodes, 'no commits in pull request (?)'); + assert( + pullRequest.commits.nodes.length === pullRequest.commits.totalCount, + "no replicating today, github is fucked up and the commit pagination doesn't fucking work", + ); + return { + type: ENTITY_TYPE.MERGE_REQUEST, + id: pullRequest.number.toString(), + content: pullRequest.body, + title: pullRequest.title, + repo: this.repo, + state: { + OPEN: MERGE_REQUEST_STATE.OPEN, + CLOSED: MERGE_REQUEST_STATE.CLOSED, + MERGED: MERGE_REQUEST_STATE.MERGED, + }[pullRequest.state], + mergability: { + MERGEABLE: MERGE_REQUEST_MERGABILITY.MERGEABLE, + CONFLICTING: MERGE_REQUEST_MERGABILITY.CONFLICTING, + UNKNOWN: null, + }[pullRequest.mergeable], + // for some fucking reason GitHub declared that the array of commits could contain null values + commits: (pullRequest.commits.nodes.filter((n) => !!n) as GHPullRequestCommit[]) + .map((n) => n.commit) + .map((c) => ({ + type: ENTITY_TYPE.COMMIT, + id: c.oid, + title: c.messageHeadline, + content: c.messageBody, + repo: this.repo, + url: `${this.repo.url}/commit/${c.oid}`, + patchURL: `${this.repo.url}/commit/${c.oid}.patch`, + patchContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${c.oid}.patch`), + diffURL: `${this.repo.url}/commit/${c.oid}.diff`, + diffContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${c.oid}.diff`), + })), + isDraft: pullRequest.isDraft, + url: `${this.repo.url}/pull/${pullRequest.number}`, + }; + } + public async getMergeRequest(number: string): Promise { const resp = await this.vendorMgr._doRequest( ` @@ -187,45 +235,51 @@ export default class GitHubRepoManager implements RepoManager { ); assert(resp.repository, 'no repository'); assert(resp.repository.pullRequest, 'no pull request'); - const { pullRequest } = resp.repository; - assert(pullRequest.commits.nodes, 'no commits in pull request (?)'); - assert( - pullRequest.commits.nodes.length === pullRequest.commits.totalCount, - "no replicating today, github is fucked up and the commit pagination doesn't fucking work", + return this._parsePR(resp.repository.pullRequest); + } + + public async replicateMergeRequest( + mergeRequest: MergeRequest, + destBranch: string, + targetBranch: string, + ) { + const resp = await this.vendorMgr._doRequest( + ` + mutation ($input: CreatePullRequestInput!) { + createPullRequest(input: $input) { + pullRequest { + number + title + body + isDraft + mergeable + state + commits(first: 250) { + nodes { + commit { + messageHeadline + messageBody + oid + } + } + totalCount + } + } + } + } + `, + { + input: { + repositoryId: this.repoId, + title: mergeRequest.title, + body: mergeRequest.content, + baseRefName: targetBranch, + headRefName: destBranch, + }, + }, ); - return { - type: ENTITY_TYPE.MERGE_REQUEST, - id: number, - content: pullRequest.body, - title: pullRequest.title, - repo: this.repo, - state: { - OPEN: MERGE_REQUEST_STATE.OPEN, - CLOSED: MERGE_REQUEST_STATE.CLOSED, - MERGED: MERGE_REQUEST_STATE.MERGED, - }[pullRequest.state], - mergability: { - MERGEABLE: MERGE_REQUEST_MERGABILITY.MERGEABLE, - CONFLICTING: MERGE_REQUEST_MERGABILITY.CONFLICTING, - UNKNOWN: null, - }[pullRequest.mergeable], - // for some fucking reason GitHub declared that the array of commits could contain null values - commits: (pullRequest.commits.nodes.filter((n) => !!n) as GHPullRequestCommit[]) - .map((n) => n.commit) - .map((c) => ({ - type: ENTITY_TYPE.COMMIT, - id: c.oid, - title: c.messageHeadline, - content: c.messageBody, - repo: this.repo, - url: `${this.repo.url}/commit/${c.oid}`, - patchURL: `${this.repo.url}/commit/${c.oid}.patch`, - patchContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${c.oid}.patch`), - diffURL: `${this.repo.url}/commit/${c.oid}.diff`, - diffContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${c.oid}.diff`), - })), - isDraft: pullRequest.isDraft, - url: `${this.repo.url}/pull/${pullRequest.number}`, - }; + assert(resp.createPullRequest, 'no pull request creation object'); + assert(resp.createPullRequest.pullRequest, 'no pull request'); + return this._parsePR(resp.createPullRequest.pullRequest); } } diff --git a/src/vendor/gitlab/repomgr.ts b/src/vendor/gitlab/repomgr.ts index 19922c9..1d80cbc 100644 --- a/src/vendor/gitlab/repomgr.ts +++ b/src/vendor/gitlab/repomgr.ts @@ -18,7 +18,13 @@ import { MERGE_REQUEST_MERGABILITY, ENTITY_TYPE, } from '../../types'; -import { GLIssue, GLMutation, GLMutationCreateIssueArgs } from './api-types'; +import { + GLIssue, + GLMergeRequest, + GLMutation, + GLMutationCreateIssueArgs, + GLMutationMergeRequestCreateArgs, +} from './api-types'; import { GL4MergeRequestCommit } from './rest-api-types'; import GitLabVendorManager from './vendormgr'; import assert from 'assert'; @@ -117,7 +123,7 @@ export default class GitHubRepoManager implements RepoManager { return this._parseIssue(issue); } - public _parseIssue(issue: GLIssue): Issue { + protected _parseIssue(issue: GLIssue): Issue { return { type: ENTITY_TYPE.ISSUE, id: issue.iid, @@ -165,39 +171,16 @@ export default class GitHubRepoManager implements RepoManager { return this._parseIssue(resp.createIssue.issue); } - public async getMergeRequest(number: string): Promise { - const resp = await this.vendorMgr._doRequest_gql( - ` - query ($path: ID!, $id: String!) { - project(fullPath: $path) { - mergeRequest(iid: $id) { - iid - title - description - state - mergeStatus - workInProgress - } - } - } - `, - { - path: this.repoPath, - id: number, - }, - ); - assert(resp.project, 'no project'); - assert(resp.project.mergeRequest, 'no merge request'); - const { mergeRequest } = resp.project; + protected async _parseMR(mergeRequest: GLMergeRequest): Promise { // for some fucking reason this is not available under the GQL API // https://gitlab.com/gitlab-org/gitlab/-/issues/300780 const commits = await this.vendorMgr._doRequest_v4( 'GET', - `projects/${encodeURIComponent(this.repoId)}/merge_requests/${number}/commits`, + `projects/${encodeURIComponent(this.repoId)}/merge_requests/${mergeRequest.iid}/commits`, ); return { type: ENTITY_TYPE.MERGE_REQUEST, - id: number, + id: mergeRequest.iid, content: mergeRequest.description || '', title: mergeRequest.title, repo: this.repo, @@ -231,4 +214,71 @@ export default class GitHubRepoManager implements RepoManager { url: `${this.repo.url}/-/merge_requests/${mergeRequest.iid}`, }; } + + public async getMergeRequest(number: string): Promise { + const resp = await this.vendorMgr._doRequest_gql( + ` + query ($path: ID!, $id: String!) { + project(fullPath: $path) { + mergeRequest(iid: $id) { + iid + title + description + state + mergeStatus + workInProgress + } + } + } + `, + { + path: this.repoPath, + id: number, + }, + ); + assert(resp.project, 'no project'); + assert(resp.project.mergeRequest, 'no merge request'); + const { mergeRequest } = resp.project; + return this._parseMR(mergeRequest); + } + + public async replicateMergeRequest( + mergeRequest: MergeRequest, + destBranch: string, + targetBranch: string, + ) { + const resp = await this.vendorMgr._doRequest_gql( + ` + mutation ($input: MergeRequestCreateInput!) { + mergeRequestCreate(input: $input) { + mergeRequest { + iid + title + description + state + mergeStatus + workInProgress + } + errors + } + } + `, + { + input: { + projectPath: this.repoPath, + sourceBranch: destBranch, + targetBranch: targetBranch, + title: mergeRequest.title, + description: mergeRequest.content, + }, + }, + ); + assert(resp.mergeRequestCreate, 'creating MR failed for unknown reason'); + assert( + !Array.isArray(resp.mergeRequestCreate.errors) || resp.mergeRequestCreate.errors.length === 0, + `GitLab said: ${(resp.mergeRequestCreate.errors || []).map((e) => `"${e}"`).join(', ')}`, + ); + assert(resp.mergeRequestCreate.mergeRequest, 'creating MR failed for unknown reason (2)'); + return this._parseMR(resp.mergeRequestCreate.mergeRequest); + } }