UNPKG

@ts-common/azure-js-dev-tools

Version:

Developer dependencies for TypeScript related projects

1,240 lines (1,104 loc) 82.7 kB
/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. */ import { Octokit } from "@octokit/rest"; import * as fs from "fs"; import { contains, first, map, removeFirst, where } from "./arrays"; import { URLBuilder } from "./url"; import { StringMap } from "./common"; /** * The name and optional organization that the repository belongs to. */ export interface Repository { /** * The entity that owns the repository. */ owner: string; /** * The name of the repository. */ name: string; } /** * Get a GitHubRepository object from the provided string or GitHubRepository object. * @param repository The repository name or object. */ export function getRepository(repository: string | Repository): Repository { let result: Repository; if (!repository) { result = { name: repository, owner: "" }; } else if (typeof repository === "string") { let slashIndex: number = repository.indexOf("/"); if (slashIndex === -1) { slashIndex = repository.indexOf("\\"); } result = { name: repository.substr(slashIndex + 1), owner: slashIndex === -1 ? "" : repository.substr(0, slashIndex) }; } else { result = repository; } return result; } export function getGithubRepositoryUrl(repository: Repository): string { return `https://github.com/${repository.owner}/${repository.name}`; } /** * Get whether or not the two provided repositories are equal. */ export function repositoriesAreEqual(lhs: string | Repository, rhs: string | Repository): boolean { return getRepositoryFullName(lhs).toLowerCase() === getRepositoryFullName(rhs).toLowerCase(); } /** * A comment in a GitHub repository. */ export interface GitHubComment { id: number; node_id: string; url: string; /** * The URL to the html version of this comment. */ html_url: string; /** * The body/text of this comment. */ body: string; /** * The user that made this comment. */ user: GitHubUser; /** * The timestamp for when this comment was created. */ created_at: string; /** * The timestamp for the last time that this comment was updated. */ updated_at: string; } /** * Get the full name of the provided repository. * @param repository The repository to get the full name of. */ export function getRepositoryFullName(repository: string | Repository): string { let result: string; if (!repository) { result = ""; } else if (typeof repository === "string") { result = repository; } else if (!repository.owner) { result = repository.name; } else { result = `${repository.owner}/${repository.name}`; } return result; } /** * The type of the body that GitHub sends for a pull_request webhook request. */ export interface GitHubPullRequestWebhookBody { /** * The action that the Webhook request is being sent as a result of. */ action: "assigned" | "unassigned" | "review_requested" | "review_request_removed" | "labeled" | "unlabeled" | "opened" | "edited" | "closed" | "reopened"; /** * The pull request number. */ number: number; /** * The pull request that was changed. */ pull_request: GitHubPullRequest; } export interface GitHubLabel { id: number; node_id: string; url: string; name: string; color: string; default: boolean; } export type GitHubMilestoneState = "open" | "closed"; export interface GitHubMilestone { title: string; due_on: string; number: number; open_issues: number; closed_issues: number; state: GitHubMilestoneState; } export interface GitHubSprintLabel { sprint: number; unplannedColor?: string; plannedColor?: string; startedColor?: string; } export interface GitHubSprintMilestone { milestoneNumber?: number; sprint: number; endDate: string; openIssueCount: number; open: boolean; } export type GitHubPullRequestState = "open" | "closed"; export interface GitHubPullRequest { base: GitHubPullRequestCommit; head: GitHubPullRequestCommit; merge_commit_sha?: string; id: number; labels: GitHubLabel[]; number: number; state: GitHubPullRequestState; merged?: boolean; title: string; url: string; html_url: string; diff_url: string; milestone?: GitHubMilestone; assignees?: GitHubUser[]; /** * The description for the pull request. */ body?: string; } /** * The way that a pull request will be merged. */ export type GitHubMergeMethod = "merge" | "squash" | "rebase"; export interface GitHubUser { id: number; login: string; name?: string; url: string; node_id: string; site_admin: boolean; } export interface GitHubPullRequestCommit { label: string; ref: string; sha: string; } /** * Get the label in the provided GitHubPullRequest that has the provided name. If no label is found, * then undefined will be returned. * @param githubPullRequest The pull request to look for the label in. * @param labelName The name of the label to look for. */ export function gitHubPullRequestGetLabel(githubPullRequest: GitHubPullRequest, labelName: string): GitHubLabel | undefined { return first(githubPullRequest.labels, (label: GitHubLabel) => label.name === labelName); } export function gitHubPullRequestGetLabels(githubPullRequest: GitHubPullRequest, labelNames: string | string[]): GitHubLabel[] { const labelNamesArray: string[] = (typeof labelNames === "string" ? [labelNames] : labelNames); return where(githubPullRequest.labels, (label: GitHubLabel) => contains(labelNamesArray, label.name)); } export function gitHubPullRequestGetAssignee(githubPullRequest: GitHubPullRequest, assignee: GitHubUser | string | number): GitHubUser | undefined { return first(githubPullRequest.assignees, (existingAssignee: GitHubUser) => { let isMatch: boolean; if (!assignee) { isMatch = false; } else if (typeof assignee === "number") { isMatch = (existingAssignee.id === assignee); } else if (typeof assignee === "string") { isMatch = (existingAssignee.login === assignee || existingAssignee.name === assignee); } else { isMatch = (existingAssignee.id === assignee.id); } return isMatch; }); } function getPullRequestNumber(pullRequest: number | GitHubPullRequest): number { return typeof pullRequest === "number" ? pullRequest : pullRequest.number; } function getCommentId(comment: number | GitHubComment): number { return typeof comment === "number" ? comment : comment.id; } /** * Optional parameters that can be provided to the GitHub.getMilestones() function to restrict the * returned milestones. */ export interface GitHubGetMilestonesOptions { /** * Filter the results to the milestones that are either open (true) or closed (false). If this * value is undefined, then all milestones will be returned. */ open?: boolean; } export interface GitHubCreateMilestoneOptions { endDate?: string; } /** * Optional parameters that can be provided to the GitHub.getPullRequests() function to restrict the * returned pull requests. */ export interface GitHubGetPullRequestsOptions extends Partial<Octokit.PullsListParams> { /** * Filter the results to the pull requests that are either open (true) or closed (false). If this * value is undefined, then all pull requests will be returned. */ open?: boolean; } /** * Optional parameters that can be provided to the GitHub.createPullRequest function. */ export interface GitHubCreatePullRequestOptions { title: string; /** * The description that will appear in the created pull request. */ body?: string; /** * Whether or not the maintainer of the pull request can modify the head branch. Defaults to * false. */ maintainerCanModify?: boolean; } /** * A commit that exists in a GitHub repository. */ export interface GitHubCommit { /** * The unique identifier for this commit. */ sha: string; /** * The data of the GitHub commit. */ commit: GitHubCommitData; } /** * The data of a GitHub commit. */ export interface GitHubCommitData { /** * The message of the commit. */ message: string; } export interface GitHubContent { download_url: string | null; encoding?: string | undefined; content?: string | undefined; html_url: string; sha: string; url: string; } export interface GitHubContentItem { download_url: string | null; } /** * A reference to a branch in a repository. */ export interface RepositoryBranch { /** * The owner of the repository that the branch belongs to. */ owner: string; /** * The name of the branch in the repository. */ name: string; } /** * Parse a ForkedRepositoryBranch reference from the provided value. * @param repositoryBranch The string or ForkedRepositoryBranch to parse. */ export function getRepositoryBranch(repositoryBranch: string | RepositoryBranch): RepositoryBranch { let result: RepositoryBranch; if (typeof repositoryBranch === "string") { const colonIndex: number = repositoryBranch.indexOf(":"); if (colonIndex === -1) { result = { owner: "", name: repositoryBranch }; } else { const owner: string = repositoryBranch.substring(0, colonIndex); const branchName: string = repositoryBranch.substring(colonIndex + 1); result = { owner, name: branchName }; } } else { result = repositoryBranch; } return result; } export function getRepositoryBranchFullName(repositoryBranch: string | RepositoryBranch): string { let result: string; if (!repositoryBranch || typeof repositoryBranch === "string") { result = repositoryBranch; } else if (!repositoryBranch.owner) { result = repositoryBranch.name; } else { result = `${getRepositoryFullName(repositoryBranch.owner)}:${repositoryBranch.name}`; } return result; } /** * A generic reference from a GitHub repository. This can be either a branch, tag, note, or stash. */ export interface GitHubReference { /** * This reference's full name. */ readonly ref: string; readonly node_id: string; /** * The GitHub URL for this reference. */ readonly url: string; readonly object: { /** * The type of Git object that this reference points to. */ readonly type: string; /** * The SHA that this reference points to. */ readonly sha: string; /** * The URL of the Git object that this reference points to. */ readonly url: string; }; } /** * A branch reference from a GitHub repository. */ export interface GitHubBranch extends GitHubReference { /** * The simplified name of the branch. */ readonly name: string; } export interface GitHub { /** * Get the user that is currently authenticated. * @returns The user that is currently authenticated. */ getCurrentUser(): Promise<GitHubUser>; /** * Get all of the labels in the provided repository. */ getLabels(repository: string | Repository): Promise<GitHubLabel[]>; /** * Get all of the labels that contain "-Sprint-" in the provided repository. * @param repository The repository to look in. */ getSprintLabels(repository: string | Repository): Promise<GitHubSprintLabel[]>; /** * Create a label with the provided labelName and color in the provided repository. * @param repositoryName The name of the repository where the label will be created. * @param labelName The name of the created label. * @param color The color of the created label. */ createLabel(repository: string | Repository, labelName: string, color: string): Promise<GitHubLabel>; /** * Delete the provided label from the provided repository. * @param repository The repository to delete the label from. * @param label The label name, id, or details to delete. */ deleteLabel(repository: string | Repository, label: string | number | GitHubLabel): Promise<unknown>; /** * Update the color of the label with the provided name in the provided repository. * @param repository The repository that contains the label to update. * @param labelName The name of the label to update. * @param newColor The color to update the label to. */ updateLabelColor(repository: string | Repository, labelName: string, newColor: string): Promise<unknown>; /** * Get the milestone in the provided repository with either the provided milestone number or name. */ getMilestone(repository: string | Repository, milestone: number | string): Promise<GitHubMilestone>; /** * Get all of the milestones that exist in the provided repository. * @param repository The repository to get all of the milestones of. * @returns All of the milestones that exist in the provided repository. */ getMilestones(repository: string | Repository, options?: GitHubGetMilestonesOptions): Promise<GitHubMilestone[]>; /** * Get all of the sprint milestones (milestones that begin with "Sprint-") in the provided * repository. * @param repository The repository. * @returns All of the sprint milestones in the provided repository. */ getSprintMilestones(repository: string | Repository, options?: GitHubGetMilestonesOptions): Promise<GitHubSprintMilestone[]>; /** * Create a new milestone in the provided repository. * @param repository The repository to create a new milestone in. * @param milestoneName The name of the new milestone. * @param options The optional properties to set on the created milestone. */ createMilestone(repositoryName: string | Repository, milestoneName: string, options?: GitHubCreateMilestoneOptions): Promise<GitHubMilestone>; /** * Create a new sprint milestone in the provided repository. * @param repository The repository to create the new sprint milestone in. * @param sprintNumber The number of the sprint that the milestone will be associated with. * @param sprintEndDate The last day of the sprint. */ createSprintMilestone(repository: string | Repository, sprintNumber: number, sprintEndDate: string): Promise<GitHubSprintMilestone | undefined>; /** * Update the end date of an existing milestone in the provided repository. * @param repository The repository that contains the milestone to update. * @param milestoneNumber The number id of the milestone to update. * @param newSprintEndDate The new end date to update the existing milestone to. */ updateMilestoneEndDate(repository: string | Repository, milestoneNumber: number, newSprintEndDate: string): Promise<GitHubMilestone>; updateSprintMilestoneEndDate(repository: string | Repository, sprintMilestone: GitHubSprintMilestone, newSprintEndDate: string): Promise<GitHubSprintMilestone>; closeMilestone(repository: string | Repository, milestoneNumber: number): Promise<unknown>; closeSprintMilestone(repository: string | Repository, sprintMilestone: GitHubSprintMilestone): Promise<unknown>; /** * Get the pull request from the provided repository with the provided number. * @param repository The repository to get the pull request from. */ getPullRequest(repository: string | Repository, pullRequestNumber: number): Promise<GitHubPullRequest>; /** * Get the pull requests in the provided respository. * @param repository The name of the repository. */ getPullRequests(repository: string | Repository, options?: GitHubGetPullRequestsOptions): Promise<GitHubPullRequest[]>; /** * Create a new pull request in the provided repository. * @param repository The repository to create the pull request in. * @param baseBranch The base branch that the pull request will merge into. * @param headBranch The head branch that the pull request will merge from. * @param title The title of the pull request. * @param options The optional parameters for creating a pull request. */ createPullRequest(repository: string | Repository, baseBranch: string, headBranch: string | RepositoryBranch, options: GitHubCreatePullRequestOptions): Promise<GitHubPullRequest>; updatePullRequest(repository: string | Repository, pullRequest: number | GitHubPullRequest, options?: Partial<GitHubCreatePullRequestOptions>): Promise<unknown>; /** * Close the provided pull request without merging it. * @param repository The repository that the pull request exists in. * @param pullRequest The pull request number or the pull request object to close. */ closePullRequest(repository: string | Repository, pullRequest: number | GitHubPullRequest): Promise<unknown>; /** * Merge and close the provided pull request. * @param repository The repository that the pull request exists in. * @param pullRequest The pull request number or the pull request object to merge. * @param mergeMethod The way that the pull request will be merged. */ mergePullRequest(repository: string | Repository, pullRequest: number | GitHubPullRequest, mergeMethod?: GitHubMergeMethod): Promise<unknown>; addPullRequestAssignees(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, assignees: string | GitHubUser | (string | GitHubUser)[]): Promise<unknown>; /** * Add the provided labels to the provided GitHubPullRequest. * @param repository The repository where the pull request exists. * @param githubPullRequest The GitHubPullRequest that the labels will be added to. * @param labelNamesToAdd The name of the label or labels to add to the pull request. */ addPullRequestLabels(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, labelNames: string | string[]): Promise<string[]>; /** * Remove the provided labels from the provided pull request. * @param repository The repository where the pull request exists. * @param githubPullRequest The pull request that the labels will be removed from. * @param labelNames The names of the labels to remove from the pull request. * @returns The names of the labels that were removed. */ removePullRequestLabels(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, labelNames: string | string[]): Promise<string[]>; /** * Set the milestone that the provided pull request is assigned to. * @param repository The repository where the pull request exists. * @param githubPullRequest The pull request to assign. * @param milestone The milestone to assign to the pull request. */ setPullRequestMilestone(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, milestone: number | string | GitHubMilestone): Promise<unknown>; /** * Get the comments that have been made on the provided GitHubPullRequest. * @param repository The repository where the pull request exists. * @param githubPullRequest The GitHubPullRequest to get the comments of. */ getPullRequestComments(repository: string | Repository, githubPullRequest: GitHubPullRequest | number): Promise<GitHubComment[]>; /** * Create a new comment on the provided GitHubPullRequest. * @param repository The repository where the pull request exists. * @param githubPullRequest The GitHubPullReuqest to create the new comment on. * @param commentBody The text of the comment to make. */ createPullRequestComment(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, commentBody: string): Promise<GitHubComment>; /** * Update an existing comment on the provided GitHubPullRequest. * @param repository The repository where the pull request exists. * @param githubPullRequest The GitHubPullRequest to update an existing comment on. * @param comment The updated comment. */ updatePullRequestComment(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, comment: GitHubComment | number, commentBody: string): Promise<GitHubComment>; /** * Delete an existing comment from the provided GitHubPullRequest. * @param repository The repository where the pull request exists. * @param githubPullRequest The GitHubPUllRequest to delete an existing comment from. * @param comment The comment to delete. */ deletePullRequestComment(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, comment: GitHubComment | number): Promise<unknown>; /** * Get the details of the commit with the provided unique identifier or undefined if no commit * existed with the provided identifier. * @param repository The repository that the commit exists in. * @param commit A unique identifier for the commit. */ getCommit(repository: string | Repository, commit: string): Promise<GitHubCommit | undefined>; /** * Get one file contents from Git. If the content is encoded by base64, it will be decoded. * Support private repo. * @param repository The repository that the file exists in. * @param filepath A unique name for the file. */ getContents(repository: string | Repository, filepath: string): Promise<GitHubContent | undefined | Array<GitHubContentItem>>; /** * Get all of the references (branches, tags, notes, stashes, etc.) in the provided repository. * @param repository The repository to get all of the references for. * @returns All of the references (branches, tags, notes, stashes, etc.) in the provided * repository. */ getAllReferences(repository: string | Repository): Promise<GitHubReference[]>; /** * Get all of the branches in the provided repository. * @param repository The repository to get all of the branches for. * @returns All of the branches in the provided repository. */ getAllBranches(repository: string | Repository): Promise<GitHubBranch[]>; /** * Get more information about the provided branch in the provided repository. * @param repository The repository to get the branch from. * @param branchName The name of the branch to get. */ getBranch(repository: string | Repository, branchName: string): Promise<GitHubBranch>; /** * Delete the branch with the provided name in the provided repository. * @param repository The repository to delete the branch from. * @param branchName The name of the branch to delete. */ deleteBranch(repository: string | Repository, branchName: string): Promise<unknown>; /** * Create a branch with the provided name as the provided sha in the provided repository. * @param repository The repository to create the branch in. * @param branchName The name of the branch to create. * @param branchSha The SHA/commit ID that the branch will be created at. */ createBranch(repository: string | Repository, branchName: string, branchSha: string): Promise<GitHubBranch>; } export interface FakeGitHubPullRequest extends GitHubPullRequest { comments: GitHubComment[]; } type FakeContent = |GitHubContent|GitHubContentItem[]|undefined; export class FakeRepository { public readonly labels: GitHubLabel[] = []; public readonly milestones: GitHubMilestone[] = []; public readonly pullRequests: FakeGitHubPullRequest[] = []; public readonly commits: GitHubCommit[] = []; public readonly branches: GitHubBranch[] = []; public readonly forks: FakeRepository[] = []; public readonly content: FakeContent[] = []; constructor(public readonly name: string, public readonly forkOf?: FakeRepository) { } /** * Get the fork of this repository that was created by the provided owner. * @param owner The name of the owner that created a fork of this repository. */ public getFork(owner: string): FakeRepository | undefined { const forksToSearch: FakeRepository[] = this.forkOf ? this.forkOf.forks : this.forks; return first(forksToSearch, (fork: FakeRepository) => getRepository(fork.name).owner === owner); } } export class FakeGitHub implements GitHub { private readonly users: GitHubUser[] = []; private currentUser: GitHubUser | undefined; private readonly repositories: FakeRepository[] = []; public getRepository(repository: string | Repository): FakeRepository { const repositoryFullName: string = getRepositoryFullName(repository); const fakeRepository: FakeRepository | undefined = first(this.repositories, (fakeRepository: FakeRepository) => fakeRepository.name === repositoryFullName); if (!fakeRepository) { throw new Error(`No fake repository exists with the name "${repositoryFullName}".`); } return fakeRepository; } private createRepositoryInner(repository: string | Repository, forkOf?: string | Repository): FakeRepository { const repositoryFullName: string = getRepositoryFullName(repository); let fakeRepository: FakeRepository | undefined = first(this.repositories, (fakeRepository: FakeRepository) => fakeRepository.name === repositoryFullName); if (fakeRepository) { throw new Error(`A fake repository with the name "${repositoryFullName}" already exists.`); } else { let forkOfRepository: FakeRepository | undefined; if (forkOf) { forkOfRepository = this.getRepository(forkOf); if (forkOfRepository.forkOf) { forkOfRepository = forkOfRepository.forkOf; } } fakeRepository = new FakeRepository(repositoryFullName, forkOfRepository); if (forkOfRepository) { forkOfRepository.forks.push(fakeRepository); } this.repositories.push(fakeRepository); } return fakeRepository; } public createRepository(repository: string | Repository): FakeRepository { return this.createRepositoryInner(repository); } public forkRepository(repository: string | Repository, forkedRepositoryOwner: string): FakeRepository { repository = getRepository(repository); const forkedRepository: Repository = { owner: forkedRepositoryOwner, name: repository.name, }; return this.createRepositoryInner(forkedRepository, repository); } public deleteRepository(repository: string | Repository): Promise<void> { const repositoryFullName: string = getRepositoryFullName(repository); const deletedRepository: FakeRepository | undefined = removeFirst(this.repositories, (repo: FakeRepository) => repo.name === repositoryFullName); let result: Promise<void>; if (!deletedRepository) { result = Promise.reject(new Error(`No fake repository exists with the name "${repositoryFullName}".`)); } else { if (deletedRepository.forkOf) { removeFirst(deletedRepository.forkOf.forks, (fork: FakeRepository) => fork === deletedRepository); } result = Promise.resolve(); } return result; } public createUser(username: string): GitHubUser { let user: GitHubUser | undefined = first(this.users, (user: GitHubUser) => user.login === username); if (user) { throw new Error(`A fake user with the username "${username}" already exists.`); } else { user = { id: 0, name: "Fake User Name", node_id: "Fake Node ID", login: username, url: `https://api.github.com/users/${username}`, site_admin: false }; this.users.push(user); } return user; } public getUser(username: string): GitHubUser { const user: GitHubUser | undefined = first(this.users, (user: GitHubUser) => user.login === username); if (!user) { throw new Error(`No fake user with the username "${username}" exists.`); } return user; } public setCurrentUser(username: string): void { this.currentUser = this.getUser(username); } public async getLabel(repository: string | Repository, label: string): Promise<GitHubLabel> { let result: Promise<GitHubLabel>; const labels: GitHubLabel[] = await this.getLabels(repository); const githubLabel: GitHubLabel | undefined = first(labels, (l: GitHubLabel) => l.name === label); if (!githubLabel) { result = Promise.reject(new Error(`No fake label named "${label}" found in the fake repository "${getRepositoryFullName(repository)}".`)); } else { result = Promise.resolve(githubLabel); } return result; } public getCurrentUser(): Promise<GitHubUser> { return this.currentUser ? Promise.resolve(this.currentUser) : Promise.reject(new Error(`No fake current user has been set.`)); } public getLabels(repository: string | Repository): Promise<GitHubLabel[]> { return toPromise(() => this.getRepository(repository).labels); } public async getSprintLabels(repository: string | Repository): Promise<GitHubSprintLabel[]> { const labels: GitHubLabel[] = await this.getLabels(repository); return getSprintLabels(labels); } public async createLabel(repository: string | Repository, labelName: string, color: string): Promise<GitHubLabel> { let result: Promise<GitHubLabel>; if (!labelName) { result = Promise.reject(new Error(`labelName cannot be undefined or empty.`)); } else if (!color) { result = Promise.reject(new Error(`label color cannot be undefined or empty.`)); } else if (color.startsWith("#")) { result = Promise.reject(new Error(`Validation Failed`)); } else { const fakeRepository: FakeRepository = this.getRepository(repository); const label: GitHubLabel = { id: 0, default: false, node_id: "fake label node_id", url: "fake label url", name: labelName, color, }; fakeRepository.labels.push(label); result = Promise.resolve(label); } return result; } public async deleteLabel(repository: string | Repository, label: string | GitHubLabel): Promise<void> { const labelName: string = (!label || typeof label === "string") ? label : label.name; let result: Promise<void>; if (!labelName) { result = Promise.reject(new Error(`label cannot be undefined or an empty string.`)); } else { const fakeRepository: FakeRepository = this.getRepository(repository); const removedLabel: GitHubLabel | undefined = removeFirst(fakeRepository.labels, (label: GitHubLabel) => label.name === labelName); if (!removedLabel) { result = Promise.reject(new Error(`No label named "${labelName}" found in the fake repository "${getRepositoryFullName(repository)}".`)); } else { result = Promise.resolve(); } } return result; } public async updateLabelColor(repository: string | Repository, labelName: string, newColor: string): Promise<unknown> { const fakeRepository: FakeRepository = this.getRepository(repository); const label: GitHubLabel | undefined = first(fakeRepository.labels, (label: GitHubLabel) => label.name === labelName); let result: Promise<unknown>; if (!label) { result = Promise.reject(new Error(`No label named "${labelName}" found in the fake repository "${getRepositoryFullName(repository)}".`)); } else { label.color = newColor; result = Promise.resolve(); } return result; } public async getMilestone(repository: string | Repository, milestone: string | number): Promise<GitHubMilestone> { const milestones: GitHubMilestone[] = await this.getMilestones(repository); let result: Promise<GitHubMilestone>; if (typeof milestone === "string") { const milestoneMatch: GitHubMilestone | undefined = first(milestones, (m: GitHubMilestone) => m.title === milestone); if (!milestoneMatch) { result = Promise.reject(new Error(`No milestone found with the name "${milestone}" in the fake repository "${getRepositoryFullName(repository)}".`)); } else { result = Promise.resolve(milestoneMatch); } } else { const milestoneMatch: GitHubMilestone | undefined = first(milestones, (m: GitHubMilestone) => m.number === milestone); if (!milestoneMatch) { result = Promise.reject(new Error(`No milestone found with the id number ${milestone} in the fake repository "${getRepositoryFullName(repository)}".`)); } else { result = Promise.resolve(milestoneMatch); } } return result; } public async getMilestones(repository: string | Repository, options?: GitHubGetMilestonesOptions): Promise<GitHubMilestone[]> { const fakeRepository: FakeRepository = this.getRepository(repository); let result: GitHubMilestone[] = fakeRepository.milestones; if (options && options.open !== undefined) { result = where(result, (milestone: GitHubMilestone) => milestone.state === (options.open ? "open" : "closed")); } return result; } public async getSprintMilestones(repository: string | Repository, options?: GitHubGetMilestonesOptions): Promise<GitHubSprintMilestone[]> { const milestones: GitHubMilestone[] = await this.getMilestones(repository, options); return githubMilestonesToSprintMilestones(milestones); } public async createMilestone(repository: string | Repository, milestoneName: string, options?: GitHubCreateMilestoneOptions): Promise<GitHubMilestone> { const fakeRepository: FakeRepository = this.getRepository(repository); const milestone: GitHubMilestone = { title: milestoneName, number: 0, due_on: addOffset(options && options.endDate || "2000-01-02"), state: "open", closed_issues: 0, open_issues: 0 }; fakeRepository.milestones.push(milestone); return milestone; } public async createSprintMilestone(repository: string | Repository, sprintNumber: number, sprintEndDate: string): Promise<GitHubSprintMilestone | undefined> { const milestoneName = getSprintMilestoneName(sprintNumber); const githubMilestone: GitHubMilestone = await this.createMilestone(repository, milestoneName, { endDate: sprintEndDate }); return githubMilestoneToSprintMilestone(githubMilestone); } public async updateMilestoneEndDate(repository: string | Repository, milestoneNumber: number, newSprintEndDate: string): Promise<GitHubMilestone> { const milestone: GitHubMilestone = await this.getMilestone(repository, milestoneNumber); milestone.due_on = addOffset(newSprintEndDate); return milestone; } public async updateSprintMilestoneEndDate(repository: string | Repository, sprintMilestone: GitHubSprintMilestone, newSprintEndDate: string): Promise<GitHubSprintMilestone> { const githubMilestone: GitHubMilestone = await this.updateMilestoneEndDate(repository, sprintMilestone.milestoneNumber!, newSprintEndDate); return githubMilestoneToSprintMilestone(githubMilestone)!; } public async closeMilestone(repository: string | Repository, milestoneNumber: number): Promise<void> { const milestone: GitHubMilestone = await this.getMilestone(repository, milestoneNumber); milestone.state = "closed"; } public closeSprintMilestone(repository: string | Repository, sprintMilestone: GitHubSprintMilestone): Promise<void> { return this.closeMilestone(repository, sprintMilestone.milestoneNumber!); } public createFakePullRequest(repository: string | Repository, pullRequest: GitHubPullRequest): FakeGitHubPullRequest { repository = getRepository(repository); const repositoryFullName: string = getRepositoryFullName(repository); const fakeRepository: FakeRepository = this.getRepository(repository); let result: FakeGitHubPullRequest; const baseBranch: RepositoryBranch = getRepositoryBranch(pullRequest.base.label); if (baseBranch.owner !== repository.owner) { throw new Error(`The owner of the repository (${repository.owner}) must be the same owner as the base branch (${baseBranch.owner}).`); } const headBranch: RepositoryBranch = getRepositoryBranch(pullRequest.head.label); if (headBranch.owner === repository.owner) { // Pull request between branches within the same repository. if (!contains(fakeRepository.branches, (branch: GitHubBranch) => branch.name === baseBranch.name)) { throw new Error(`No branch exists in the fake repository "${repositoryFullName}" with the name "${baseBranch.name}".`); } else if (!contains(fakeRepository.branches, (branch: GitHubBranch) => branch.name === headBranch.name)) { throw new Error(`No branch exists in the fake repository "${repositoryFullName}" with the name "${baseBranch.name}".`); } } else { // Pull request between branches in different repositories. const forkedRepository: FakeRepository | undefined = fakeRepository.getFork(headBranch.owner); if (!forkedRepository) { throw new Error(`No fork of the fake repository "${getRepositoryFullName(repository)}" exists for the owner "${headBranch.owner}".`); } else if (!contains(forkedRepository.branches, (branch: GitHubBranch) => branch.name === headBranch.name)) { throw new Error(`No branch exists in the forked fake repository "${forkedRepository.name}" with the name "${headBranch.name}".`); } } if (pullRequest.base.label === pullRequest.head.label) { throw new Error(`The base label ("${pullRequest.base.label}") cannot be the same as the head label ("${pullRequest.head.label}").`); } else { const existingPullRequest: FakeGitHubPullRequest | undefined = first(fakeRepository.pullRequests, (pr: FakeGitHubPullRequest) => pr.number === pullRequest.number); if (existingPullRequest) { throw new Error(`A pull request already exists in the fake repository "${getRepositoryFullName(repository)}" with the number ${pullRequest.number}.`); } else { pullRequest.body = pullRequest.body || ""; result = { ...pullRequest, comments: [], }; fakeRepository.pullRequests.push(result); } } return result; } public async createPullRequest(repository: string | Repository, baseBranch: string | RepositoryBranch, headBranch: string | RepositoryBranch, options: GitHubCreatePullRequestOptions): Promise<GitHubPullRequest> { repository = getRepository(repository); const fakeRepository: FakeRepository = this.getRepository(repository); baseBranch = getRepositoryBranch(baseBranch); if (baseBranch.owner) { throw new Error(`When creating a pull request, the provided baseBranch (${getRepositoryBranchFullName(baseBranch)}) cannot have an owner.`); } baseBranch.owner = repository.owner; headBranch = getRepositoryBranch(headBranch); if (!headBranch.owner) { headBranch.owner = repository.owner; } return this.createFakePullRequest(repository, { base: { label: getRepositoryBranchFullName(baseBranch), ref: baseBranch.name, sha: "fake-base-sha", }, diff_url: "fake-diff-url", head: { label: getRepositoryBranchFullName(headBranch), ref: headBranch.name, sha: "fake-head-sha", }, html_url: "fake-html-url", id: fakeRepository.pullRequests.length + 1, labels: [], number: fakeRepository.pullRequests.length + 1, state: "open", title: options.title, url: "fake-url", body: options.body }); } public async updatePullRequest(repository: string | Repository, pullRequest: number | GitHubPullRequest, options: Partial<GitHubCreatePullRequestOptions> = {}): Promise<unknown> { const existingPullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, getPullRequestNumber(pullRequest)); Object.assign(existingPullRequest, options); return existingPullRequest; } public async closePullRequest(repository: string | Repository, pullRequestNumber: number | GitHubPullRequest): Promise<void> { const existingPullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, getPullRequestNumber(pullRequestNumber)); existingPullRequest.state = "closed"; } public async mergePullRequest(repository: string | Repository, pullRequest: number | GitHubPullRequest, _mergeMethod: GitHubMergeMethod): Promise<void> { const existingPullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, getPullRequestNumber(pullRequest)); let result: Promise<void>; if (existingPullRequest.state === "closed") { result = Promise.reject(new Error(`The pull request (${getRepositoryFullName(repository)}/${existingPullRequest.number}) is already closed.`)); } else { existingPullRequest.state = "closed"; result = Promise.resolve(); } return result; } public async getPullRequest(repository: string | Repository, pullRequestNumber: number): Promise<FakeGitHubPullRequest> { const pullRequests: FakeGitHubPullRequest[] = await this.getPullRequests(repository); const pullRequest: FakeGitHubPullRequest | undefined = first(pullRequests, (pr: FakeGitHubPullRequest) => pr.number === pullRequestNumber); return pullRequest ? Promise.resolve(pullRequest) : Promise.reject(new Error(`No pull request found in fake repository "${getRepositoryFullName(repository)}" with number ${pullRequestNumber}.`)); } public getPullRequests(repository: string | Repository, options?: GitHubGetPullRequestsOptions): Promise<FakeGitHubPullRequest[]> { const fakeRepository: FakeRepository = this.getRepository(repository); let result: FakeGitHubPullRequest[] = fakeRepository.pullRequests; if (options) { if (options.open) { result = where(result, (pullRequest) => pullRequest.state === (options.open ? "open" : "closed")); } if (options.head) { result = where(result, (pullRequest) => pullRequest.head.ref === options.head); } } return Promise.resolve(result); } public async addPullRequestAssignees(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, assignees: string | GitHubUser | (string | GitHubUser)[]): Promise<void> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); const pullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, pullRequestNumber); if (!pullRequest.assignees) { pullRequest.assignees = []; } if (!Array.isArray(assignees)) { assignees = [assignees]; } for (const assignee of assignees) { if (typeof assignee === "string") { pullRequest.assignees.push(await this.getUser(assignee)); } else { pullRequest.assignees.push(assignee); } } } public async addPullRequestLabels(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, labelNames: string | string[]): Promise<string[]> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); const labelNamesArray: string[] = (Array.isArray(labelNames) ? labelNames : [labelNames]); const repositoryLabels: GitHubLabel[] = await this.getLabels(repository); for (const labelName of labelNamesArray) { if (!contains(repositoryLabels, (repositoryLabel: GitHubLabel) => repositoryLabel.name === labelName)) { repositoryLabels.push(await this.createLabel(repository, labelName, "ededed")); } } const pullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, pullRequestNumber); const pullRequestLabels: GitHubLabel[] = pullRequest.labels; const pullRequestLabelNames: string[] = map(pullRequestLabels, (label: GitHubLabel) => label.name); const labelNamesAddedToPullRequest: string[] = where(labelNamesArray, (labelName: string) => !contains(pullRequestLabelNames, labelName)); if (labelNamesAddedToPullRequest.length > 0) { pullRequest.labels.push(...await Promise.all(map(labelNamesAddedToPullRequest, (labelName: string) => this.getLabel(repository, labelName)))); } return labelNamesAddedToPullRequest; } public async removePullRequestLabels(repository: string | Repository, githubPullRequest: number | GitHubPullRequest, labelNames: string | string[]): Promise<string[]> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); const pullRequest: FakeGitHubPullRequest = await this.getPullRequest(repository, pullRequestNumber); const labelNamesToRemove: string[] = (Array.isArray(labelNames) ? labelNames : [labelNames]); const currentLabelNames: string[] = map(pullRequest.labels, (label: GitHubLabel) => label.name); const removedLabelNames: string[] = where(currentLabelNames, (labelName: string) => contains(labelNamesToRemove, labelName)); pullRequest.labels = where(pullRequest.labels, (label: GitHubLabel) => !contains(labelNamesToRemove, label.name)); return removedLabelNames; } public setPullRequestMilestone(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, milestone: string | number | GitHubMilestone): Promise<unknown> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); return this.getPullRequest(repository, pullRequestNumber) .then((pullRequest: FakeGitHubPullRequest) => { let milestonePromise: Promise<GitHubMilestone>; if (typeof milestone === "string" || typeof milestone === "number") { milestonePromise = this.getMilestone(repository, milestone); } else { milestonePromise = Promise.resolve(milestone); } milestonePromise.then((githubMilestone: GitHubMilestone) => { pullRequest.milestone = githubMilestone; }); }); } public getPullRequestComments(repository: string | Repository, githubPullRequest: GitHubPullRequest | number): Promise<GitHubComment[]> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); return this.getPullRequest(repository, pullRequestNumber) .then((fakePullRequest: FakeGitHubPullRequest) => fakePullRequest.comments); } public createPullRequestComment(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, commentBody: string): Promise<GitHubComment> { return this.getPullRequestComments(repository, githubPullRequest) .then((comments: GitHubComment[]) => { return this.getCurrentUser() .then((currentUser: GitHubUser) => { const newComment: GitHubComment = { id: comments.length + 1, node_id: "fake_node_id", user: currentUser, html_url: "fake_html_url", url: "fake_url", body: commentBody, created_at: "fake_created_at", updated_at: "fake_updated_at", }; comments.push(newComment); return newComment; }); }); } public updatePullRequestComment(repository: string | Repository, githubPullRequest: GitHubPullRequest | number, comment: number | GitHubComment, commentBody: string): Promise<GitHubComment> { return this.getPullRequestComments(repository, githubPullRequest) .then((comments: GitHubComment[]) => { let result: Promise<GitHubComment>; const commentId: number = getCommentId(comment); const commentToUpdate: GitHubComment | undefined = first(comments, (existingComment: GitHubComment) => existingComment.id === commentId); if (!commentToUpdate) { result = Promise.reject(new Error(`No comment found with the ID ${commentId}.`)); } else { commentToUpdate.body = commentBody; result = Promise.resolve(commentToUpdate); } return result; }); } public deletePullRequestComment(repository: string | Repository, githubPullRequest: number | GitHubPullRequest, comment: number | GitHubComment): Promise<unknown> { const pullRequestNumber: number = getPullRequestNumber(githubPullRequest); return this.getPullRequest(repository, pullRequestNumber) .then((pullRequest: FakeGitHubPullRequest) => { let result: Promise<unknown>; const commentId: number = getCommentId(comment); if (!contains(pullRequest.comments, (existingComment: GitHubComment) => existingComment.id === commentId)) { result = Promise.reject(new Error(`No comment was found with the id ${commentId}.`)); } else { pullRequest.comments = where(pullRequest.comments, (existingComment: GitHubComment) => existingComment.id !== commentId); result = Promise.resolve(); } return result; }); } public getCommit(repository: string | Repository, commitId: string): Promise<GitHubCommit | undefined> { return toPromise(() => { const fakeRepository: FakeRepository = this.getRepository(repository); return first(fakeRepository.commits, (commit: GitHubCommit) => commit.sha.startsWith(commitId)); }); } public getContents(repository: string | Repository, filepath: string): Promise<GitHubContent | undefined | Array<GitHubContentItem>> { if (filepath !== undefined) { return toPromise(() => { const fakeRepository: FakeRepository = this.getRepository(repository); return first(fakeRepository.content); }); } return toPromise(() => { const fakeRepository: FakeRepository = this.getRepository(repository); return first(fakeRepository.content); }); } public createCommit(repository: string | Repository, commitId: string, message: string): Promise<unknown> { const fakeRepository: FakeRepository = this.getRepository(repository); fakeRepository.commits.push({ sha: commitId, commit: { message } }); return Promise.resolve(); } public getAllReferences(repository: string | Repository): Promise<GitHubReference[]> { return toPromise(() => this.getRepository(repository).branches); } public getAllBranches(repository: string | Repository): Promise<GitHubBranch[]> { return this.getAllReferences(repository) .then(referencesToBranches); } public getBranch(repository: string | Repository, branchName: string): Promise<GitHubBranch> { return toPromise(() => { const fakeRepos