/* * 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, Repo, Commit, ACTOR_TYPE, ISSUE_STATE, MergeRequest, MERGE_REQUEST_STATE, MERGE_REQUEST_MERGABILITY, ENTITY_TYPE, } from '../../types'; import { GLIssue, GLMergeRequest, GLMutation, GLMutationCreateIssueArgs, GLMutationMergeRequestCreateArgs, } from './api-types'; import { GL4MergeRequestCommit } from './rest-api-types'; import GitLabVendorManager from './vendormgr'; import assert from 'assert'; export default class GitHubRepoManager implements RepoManager { vendorMgr: GitLabVendorManager; repo: Repo; repoPath: string; repoId: string; constructor(vendorMgr: GitLabVendorManager, repoPath: string) { this.vendorMgr = vendorMgr; const mobj = /^([^/\s]+)\/([^\s]+)$/.exec(repoPath); assert(mobj, 'invalid repo path'); // "small path" may be just repository name // or the whole path except the top-level owner const [, owner, repoSmallPath] = mobj; this.repo = { vendor: this.vendorMgr.vendor, owner: { type: ACTOR_TYPE.UNKNOWN, username: owner, vendor: this.vendorMgr.vendor, }, name: repoSmallPath, url: `https://${this.vendorMgr.vendor.domain}/${repoPath}`, }; this.repoPath = repoPath; this.repoId = ''; // for strict null check, is replaced in .initialize } public async initialize() { const meta = await this.vendorMgr._doRequest_gql( ` query ($path: ID!, $ownerStr: String!, $ownerID: ID!) { project(fullPath: $path) { id path } group(fullPath: $ownerID) { id } user(username: $ownerStr) { id } } `, { path: this.repoPath, // there's no way to check if the namespace is a user or group, // so we check if there's anyone using that username // but one is a String, and one is an ID // and these types just don't overlap 🤷‍♀️ ownerStr: this.repo.owner.username, ownerID: this.repo.owner.username, }, ); assert(meta.project, 'no project'); assert(meta.group || meta.user, 'no top-level project owner'); const [, repoId] = /\/(\d+)$/.exec(meta.project.id) || [,]; assert(repoId, 'broken project id'); this.repoId = repoId; this.repo.owner.username = meta.project.path; if (meta.group) { this.repo.owner.type = ACTOR_TYPE.ORG; } else if (meta.user) { this.repo.owner.type = ACTOR_TYPE.USER; } return this; } public async getIssue(number: string): Promise { const resp = await this.vendorMgr._doRequest_gql( ` query ($path: ID!, $id: String!) { project(fullPath: $path) { issue(iid: $id) { iid title description state } } } `, { path: this.repoPath, id: number, }, ); assert(resp.project, 'no project'); assert(resp.project.issue, 'no issue'); const { issue } = resp.project; return this._parseIssue(issue); } protected _parseIssue(issue: GLIssue): Issue { return { type: ENTITY_TYPE.ISSUE, id: issue.iid, content: issue.description || '', title: issue.title, repo: this.repo, state: { opened: ISSUE_STATE.OPEN, closed: ISSUE_STATE.CLOSED, locked: ISSUE_STATE.CLOSED, all: ISSUE_STATE.CLOSED, // today's fact: gitlab api is fucked up }[issue.state], url: `${this.repo.url}/-/issues/${issue.iid}`, }; } public async replicateIssue(issue: Issue) { const resp = await this.vendorMgr._doRequest_gql( ` mutation ($input: CreateIssueInput!) { createIssue(input: $input) { issue { iid title description state } } } `, { input: { projectPath: this.repoPath, title: issue.title, description: issue.content, }, }, ); assert(resp.createIssue, 'creating issue failed for unknown reason'); assert( !Array.isArray(resp.createIssue.errors) || resp.createIssue.errors.length === 0, `GitLab said: ${(resp.createIssue.errors || []).map((e) => `"${e}"`).join(', ')}`, ); assert(resp.createIssue.issue, 'creating issue failed for unknown reason (2)'); return this._parseIssue(resp.createIssue.issue); } 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/${mergeRequest.iid}/commits`, ); return { type: ENTITY_TYPE.MERGE_REQUEST, id: mergeRequest.iid, content: mergeRequest.description || '', title: mergeRequest.title, repo: this.repo, state: { opened: MERGE_REQUEST_STATE.OPEN, closed: MERGE_REQUEST_STATE.CLOSED, merged: MERGE_REQUEST_STATE.MERGED, locked: MERGE_REQUEST_STATE.CLOSED, all: MERGE_REQUEST_STATE.CLOSED, // today's fact: gitlab api is fucked up }[mergeRequest.state], mergability: ({ can_be_merged: MERGE_REQUEST_MERGABILITY.MERGEABLE, cannot_be_merged: MERGE_REQUEST_MERGABILITY.CONFLICTING, } as { [key: string]: MERGE_REQUEST_MERGABILITY | undefined })[ mergeRequest.mergeStatus || '' ] || null, isDraft: mergeRequest.workInProgress, commits: commits.map((c) => ({ type: ENTITY_TYPE.COMMIT, id: c.id, title: c.title, content: c.message, repo: this.repo, url: `${this.repo.url}/-/commit/${c.id}`, diffURL: `${this.repo.url}/-/commit/${c.id}.diff`, diffContent: () => this.vendorMgr._http_get(`${this.repo.url}/-/commit/${c.id}.diff`), patchURL: `${this.repo.url}/-/commit/${c.id}.patch`, patchContent: () => this.vendorMgr._http_get(`${this.repo.url}/-/commit/${c.id}.patch`), })), 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); } }