full MR replication

This commit is contained in:
Laura Liberda 2021-02-21 21:29:21 +01:00
parent 20cfde5ea3
commit 859f3caf39
5 changed files with 220 additions and 77 deletions

View file

@ -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: {

View file

@ -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(

View file

@ -71,6 +71,14 @@ export interface RepoManager {
getIssue: (id: string) => Promise<Issue>;
replicateIssue: (issue: Issue) => Promise<Issue>;
getMergeRequest: (id: string) => Promise<MergeRequest>;
/** 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<MergeRequest>;
}
export enum ENTITY_TYPE {

View file

@ -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<MergeRequest> {
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<Commit>((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<MergeRequest> {
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<GHMutation, GHMutationCreatePullRequestArgs>(
`
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<Commit>((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);
}
}

View file

@ -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<MergeRequest> {
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<MergeRequest> {
// 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<GL4MergeRequestCommit[]>(
'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<MergeRequest> {
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<GLMutation, GLMutationMergeRequestCreateArgs>(
`
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);
}
}