copykitku/src/vendor/github/repomgr.ts
2021-02-27 01:04:33 +01:00

321 lines
9.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 {
RepoManager,
Issue,
Commit,
Repo,
ACTOR_TYPE,
ISSUE_STATE,
MergeRequest,
MERGE_REQUEST_STATE,
MERGE_REQUEST_MERGABILITY,
ENTITY_TYPE,
} from '../../types';
import GitHubVendorManager from './vendormgr';
import assert from 'assert';
import {
GHCommit,
GHGitObject,
GHMutation,
GHMutationCreateIssueArgs,
GHMutationCreatePullRequestArgs,
GHPullRequest,
GHPullRequestCommit,
} from './api-types';
export default class GitHubRepoManager implements RepoManager {
vendorMgr: GitHubVendorManager;
repo: Repo;
repoId: string;
constructor(vendorMgr: GitHubVendorManager, repoPath: string) {
this.vendorMgr = vendorMgr;
const mobj = /^([^/\s]+)\/([^/\s]+)$/.exec(repoPath);
assert(mobj, 'invalid repo path');
const [, owner, repoName] = mobj;
this.repo = {
vendor: this.vendorMgr.vendor,
owner: {
type: ACTOR_TYPE.UNKNOWN,
username: owner,
vendor: this.vendorMgr.vendor,
},
name: repoName,
url: `https://${this.vendorMgr.vendor.domain}/${repoPath}`,
};
this.repoId = '';
}
public async initialize() {
const meta = await this.vendorMgr._doRequest(
`
query Query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
owner {
login
__typename
}
}
}
`,
{
owner: this.repo.owner.username,
name: this.repo.name,
},
);
assert(meta.repository);
assert(meta.repository.owner);
this.repoId = meta.repository.id;
this.repo.name = meta.repository.name;
this.repo.owner.username = meta.repository.owner.login;
// @ts-ignore graphql-codegen ignores built-in graphql values
switch (meta.repository.owner.__typename) {
case 'Organization':
this.repo.owner.type = ACTOR_TYPE.ORG;
break;
case 'User':
this.repo.owner.type = ACTOR_TYPE.USER;
break;
}
return this;
}
public async getIssue(number: string): Promise<Issue> {
const resp = await this.vendorMgr._doRequest(
`
query Query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
issue(number: $number) {
number
title
body
closed
}
}
}
`,
{
owner: this.repo.owner.username,
name: this.repo.name,
number: parseInt(number, 10),
},
);
assert(resp.repository, 'no repository');
assert(resp.repository.issue, 'no issue');
const { issue } = resp.repository;
return {
type: ENTITY_TYPE.ISSUE,
id: number,
content: issue.body,
title: issue.title,
repo: this.repo,
state: issue.closed ? ISSUE_STATE.CLOSED : ISSUE_STATE.OPEN,
url: `${this.repo.url}/issues/${issue.number}`,
};
}
public async replicateIssue(issue: Issue) {
const resp = await this.vendorMgr._doRequest<GHMutation, GHMutationCreateIssueArgs>(
`
mutation ($input: CreateIssueInput!) {
createIssue(input: $input) {
issue {
number
title
body
closed
}
}
}
`,
{
input: {
repositoryId: this.repoId,
title: issue.title,
body: issue.content,
},
},
);
assert(resp.createIssue, 'creating issue failed');
assert(resp.createIssue.issue, 'creating issue failed');
const replicated = resp.createIssue.issue;
return {
type: ENTITY_TYPE.ISSUE as const,
id: replicated.number.toString(),
content: replicated.body,
title: replicated.title,
repo: this.repo,
state: replicated.closed ? ISSUE_STATE.CLOSED : ISSUE_STATE.OPEN,
url: `${this.repo.url}/issues/${replicated.number}`,
};
}
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((c) => this._parseCommit(c)),
isDraft: pullRequest.isDraft,
url: `${this.repo.url}/pull/${pullRequest.number}`,
};
}
public async getMergeRequest(number: string): Promise<MergeRequest> {
const resp = await this.vendorMgr._doRequest(
`
query Query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
number
title
body
isDraft
mergeable
state
commits(first: 250) {
nodes {
commit {
messageHeadline
messageBody
oid
}
}
totalCount
}
}
}
}
`,
{
owner: this.repo.owner.username,
name: this.repo.name,
number: parseInt(number, 10),
},
);
assert(resp.repository, 'no repository');
assert(resp.repository.pullRequest, 'no pull request');
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,
},
},
);
assert(resp.createPullRequest, 'no pull request creation object');
assert(resp.createPullRequest.pullRequest, 'no pull request');
return this._parsePR(resp.createPullRequest.pullRequest);
}
protected _parseCommit(commit: GHCommit): Commit {
return {
type: ENTITY_TYPE.COMMIT,
id: commit.oid,
title: commit.messageHeadline,
content: commit.messageBody,
repo: this.repo,
url: `${this.repo.url}/commit/${commit.oid}`,
patchURL: `${this.repo.url}/commit/${commit.oid}.patch`,
patchContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${commit.oid}.patch`),
diffURL: `${this.repo.url}/commit/${commit.oid}.diff`,
diffContent: () => this.vendorMgr._http_get(`${this.repo.url}/commit/${commit.oid}.diff`),
};
}
public async getCommit(oid: string) {
const resp = await this.vendorMgr._doRequest(
`
query Query($owner: String!, $name: String!, $oid: GitObjectID!) {
repository(owner: $owner, name: $name) {
object(oid: $oid) {
__typename
... on Commit {
messageHeadline
messageBody
oid
}
}
}
}
`,
{
owner: this.repo.owner.username,
name: this.repo.name,
oid: oid,
},
);
assert(resp.repository, 'no repository');
assert(resp.repository.object, 'no repository object');
const object = resp.repository.object as GHGitObject | GHCommit;
assert('__typename' in object && object.__typename === 'Commit', 'object is not a commit (?)');
return this._parseCommit(object);
}
}