/* * 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 { 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 { 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( ` 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 { 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 { 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( ` 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); } }