321 lines
9.1 KiB
TypeScript
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);
|
|
}
|
|
}
|