diff --git a/.gitignore b/.gitignore index e5eedf056..a016cc008 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,8 @@ test-results.json source/_danger.d.tse source/_danger.d.ts tests.json + +# IDEs +.idea + +/*.sh diff --git a/.travis.yml b/.travis.yml index 01875faee..5b9084303 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ matrix: - yarn flow check # Checks every example dangerfile can run in `danger runner`. - - node_js: "8.4" + - node_js: "8" script: - yarn build - node scripts/run-fixtures.js @@ -67,7 +67,7 @@ matrix: source/platforms/git/_tests/local_dangerfile_example.ts || echo "Skipping Danger Local for non PR run"' # Create some fake projects at runtime - - node_js: "8.12" + - node_js: "8" script: - echo "This is only for Integration tests on two blank projects" - yarn build diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fddf653a..b8eca3b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Add support for AppCenter - [@mrndjo] +- Adds GitLab & GitLab CI support. - [@notjosh], [@bigkraig], [@jamime] + # 7.1.4 - Un-hardcodes the repo in `danger.github.utils.createOrUpdatePR`- [@ds300] @@ -1634,3 +1636,4 @@ Not usable for others, only stubs of classes etc. - [@orta] [@dblandin]: https://github.com/dblandin [@paulmelnikow]: https://github.com/paulmelnikow [@ds300]: https://github.com/ds300 +[@jamime]: https://github.com/jamime diff --git a/README.md b/README.md index dec974ef9..0f4204398 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ review. You can use Danger to codify your teams norms, leaving humans to think about harder problems. -Danger JS works with GitHub or BitBucket Server for code review, then with: Travis CI, Circle CI, GitHub Actions, -Semaphore, Jenkins, Docker Cloud, Bitrise, surf-build, Codeship, Drone, Buildkite, Nevercode, buddybuild, TeamCity, -Visual Studio Team Services, Screwdriver, Concourse, Netlify, CodeBuild, Codefresh or AppCenter. +Danger JS works with GitHub or BitBucket Server for code review, then with: Travis CI, GitLab CI, Circle CI, GitHub +Actions, Semaphore, Jenkins, Docker Cloud, Bitrise, surf-build, Codeship, Drone, Buildkite, Nevercode, buddybuild, +TeamCity, Visual Studio Team Services, Screwdriver, Concourse, Netlify, CodeBuild, Codefresh or AppCenter. [![npm](https://img.shields.io/npm/v/danger.svg)](https://www.npmjs.com/package/danger) [![Build Status](https://travis-ci.org/danger/danger-js.svg?branch=master)](https://travis-ci.org/danger/danger-js) @@ -83,7 +83,7 @@ it compiles. You can run your dev copy of danger against a PR by running: ```sh -yarn build; node --inspect distribution/source/commands/danger-pr.js https://github.com/danger/danger-js/pull/817 +yarn build; node --inspect distribution/commands/danger-pr.js https://github.com/danger/danger-js/pull/817 ``` ### How does Danger JS work? diff --git a/package.json b/package.json index d1a05d713..07cb4c9f4 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@types/lodash.mapvalues": "^4.6.6", "@types/lodash.memoize": "^4.1.3", "@types/micromatch": "^3.1.0", + "@types/nock": "^10.0.3", "@types/node": "^10.11.3", "@types/node-fetch": "^2.1.2", "@types/p-limit": "^2.0.0", @@ -115,6 +116,7 @@ "jest-json-reporter": "^1.2.2", "lint-staged": "^7.3.0", "madge": "^3.2.0", + "nock": "^10.0.6", "pkg": "^4.3.4", "prettier": "^1.14.2", "release-it": "^7.6.1", @@ -134,6 +136,7 @@ "commander": "^2.18.0", "debug": "^4.1.1", "get-stdin": "^6.0.0", + "gitlab": "^6.0.0", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1", "hyperlinker": "^1.0.0", diff --git a/source/ci_source/providers/GitLabCI.ts b/source/ci_source/providers/GitLabCI.ts new file mode 100644 index 000000000..c0ebc5a36 --- /dev/null +++ b/source/ci_source/providers/GitLabCI.ts @@ -0,0 +1,30 @@ +import { Env, CISource } from "../ci_source" +import { ensureEnvKeysExist, ensureEnvKeysAreInt } from "../ci_source_helpers" + +export class GitLabCI implements CISource { + constructor(private readonly env: Env) {} + + get name(): string { + return "GitLab CI" + } + + get isCI(): boolean { + return ensureEnvKeysExist(this.env, ["GITLAB_CI"]) + } + + get isPR(): boolean { + const mustHave = ["CI_MERGE_REQUEST_IID", "CI_PROJECT_PATH"] + const mustBeInts = ["CI_MERGE_REQUEST_IID"] + return ensureEnvKeysExist(this.env, mustHave) && ensureEnvKeysAreInt(this.env, mustBeInts) + } + + get pullRequestID(): string { + return this.env.CI_MERGE_REQUEST_IID + } + + get repoSlug(): string { + return this.env.CI_PROJECT_PATH + } +} + +// See https://docs.gitlab.com/ee/ci/variables/predefined_variables.html diff --git a/source/ci_source/providers/index.ts b/source/ci_source/providers/index.ts index b61765aee..106a40081 100644 --- a/source/ci_source/providers/index.ts +++ b/source/ci_source/providers/index.ts @@ -11,6 +11,7 @@ import { DockerCloud } from "./DockerCloud" import { Drone } from "./Drone" import { FakeCI } from "./Fake" import { GitHubActions } from "./GitHubActions" +import { GitLabCI } from "./GitLabCI" import { Jenkins } from "./Jenkins" import { Netlify } from "./Netlify" import { Nevercode } from "./Nevercode" @@ -24,6 +25,7 @@ import { VSTS } from "./VSTS" const providers = [ FakeCI, GitHubActions, + GitLabCI, Travis, Circle, Semaphore, @@ -49,6 +51,7 @@ const providers = [ // Mainly used for Dangerfile linting const realProviders = [ GitHubActions, + GitLabCI, Travis, Circle, Semaphore, diff --git a/source/commands/ci/runner.ts b/source/commands/ci/runner.ts index 992b3d990..f926b3dce 100644 --- a/source/commands/ci/runner.ts +++ b/source/commands/ci/runner.ts @@ -58,6 +58,7 @@ export const runRunner = async (app: SharedCLI, config?: Partial) if (platform) { const dangerJSONDSL = await jsonDSLGenerator(platform, source, app) + d({ dangerJSONDSL }) const execConfig: ExecutorOptions = { stdoutOnly: !platform.supportsCommenting() || app.textOnly, verbose: app.verbose, diff --git a/source/commands/danger-pr.ts b/source/commands/danger-pr.ts index 7e2ae8409..a553773b0 100644 --- a/source/commands/danger-pr.ts +++ b/source/commands/danger-pr.ts @@ -14,6 +14,7 @@ import { prepareDangerDSL } from "./utils/runDangerSubprocess" import { runRunner } from "./ci/runner" import { Platform, getPlatformForEnv } from "../platforms/platform" import { CISource } from "../ci_source/ci_source" +import { getGitLabAPICredentialsFromEnv } from "../platforms/gitlab/GitLabAPI" const d = debug("pr") const log = console.log @@ -25,6 +26,8 @@ interface App extends SharedCLI { js?: boolean } +const gitLabApiCredentials = getGitLabAPICredentialsFromEnv(process.env) + program .usage("[options] ") .description("Emulate running Danger against an existing GitHub Pull Request.") @@ -34,9 +37,15 @@ program .on("--help", () => { log("\n") log(" Docs:") - if (!process.env["DANGER_GITHUB_API_TOKEN"] && !process.env["DANGER_BITBUCKETSERVER_HOST"]) { + if ( + !process.env["DANGER_GITHUB_API_TOKEN"] && + !process.env["DANGER_BITBUCKETSERVER_HOST"] && + !gitLabApiCredentials.token + ) { log("") - log(" You don't have a DANGER_GITHUB_API_TOKEN set up, this is optional, but TBH, you want to do this.") + log( + " You don't have a DANGER_GITHUB_API_TOKEN/DANGER_GITLAB_API_TOKEN set up, this is optional, but TBH, you want to do this." + ) log(" Check out: http://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile") log("") } @@ -60,13 +69,14 @@ if (program.args.length === 0) { console.error("Please include a PR URL to run against") process.exitCode = 1 } else { - const customHost = process.env["DANGER_GITHUB_HOST"] || process.env["DANGER_BITBUCKETSERVER_HOST"] || "github" + const customHost = + process.env["DANGER_GITHUB_HOST"] || process.env["DANGER_BITBUCKETSERVER_HOST"] || gitLabApiCredentials.host // this defaults to https://gitlab.com // Allow an ambiguous amount of args to find the PR reference - const findPR = program.args.find(a => a.includes(customHost)) + const findPR = program.args.find(a => a.includes(customHost) || a.includes("github")) if (!findPR) { - console.error(`Could not find an arg which mentioned GitHub or BitBucket Server.`) + console.error(`Could not find an arg which mentioned GitHub, BitBucket Server, or GitLab.`) process.exitCode = 1 } else { const pr = pullRequestParser(findPR) @@ -86,7 +96,18 @@ if (program.args.length === 0) { d(`executing dangerfile at ${dangerfilePath(program)}`) } const source = new FakeCI({ DANGER_TEST_REPO: pr.repo, DANGER_TEST_PR: pr.pullRequestNumber }) - const platform = getPlatformForEnv(process.env, source, /* requireAuth */ false) + const platform = getPlatformForEnv( + { + ...process.env, + // Inject a platform hint, its up to getPlatformForEnv to decide if the environment is suitable for the + // requested platform. Because we have a URL we can determine with greater accuracy what platform that the + // user is attempting to test. This complexity is required because danger-pr defaults to using + // un-authenticated GitHub where typically when using FakeCI we want to use Fake(Platform) e.g. when running + // danger-local + DANGER_PR_PLATFORM: pr.platform, + }, + source + ) if (isJSON) { d("getting just the JSON/JS DSL") diff --git a/source/danger.d.ts b/source/danger.d.ts index 9655cc8f4..16bb7a349 100644 --- a/source/danger.d.ts +++ b/source/danger.d.ts @@ -406,6 +406,8 @@ interface DangerDSLJSONType { github?: GitHubDSL /** The data only version of BitBucket Server DSL */ bitbucket_server?: BitBucketServerJSONDSL + /** The data only version of GitLab DSL */ + gitlab?: GitLabDSL /** * Used in the Danger JSON DSL to pass metadata between * processes. It will be undefined when used inside the Danger DSL @@ -468,12 +470,23 @@ interface DangerDSLType { * comments and reviews on the PR, related issues, commits, comments * and activities. * - * Strictly speaking, `bitbucket_server` is a nullable type, if you are using - * GitHub then it will be undefined. For the DSL convenience sake though, it + * Strictly speaking, `bitbucket_server` is a nullable type, if you are not using + * BitBucket Server then it will be undefined. For the DSL convenience sake though, it * is classed as non-nullable */ readonly bitbucket_server: BitBucketServerDSL + /** + * The GitLab metadata. This covers things like PR info, + * comments and reviews on the MR, commits, comments + * and activities. + * + * Strictly speaking, `gitlab` is a nullable type, if you are not using + * GitLab then it will be undefined. For the DSL convenience sake though, it + * is classed as non-nullable + */ + readonly gitlab: GitLabDSL + /** * Functions which are globally useful in most Dangerfiles. Right * now, these functions are around making sentences of arrays, or @@ -1213,6 +1226,231 @@ interface GitHubReviewers { teams: any[] } +// TODO: extract out from BitBucket specifically, or create our own type +// getPlatformReviewDSLRepresentation +interface GitLabJSONDSL { + metadata: RepoMetaData + mr: GitLabMR + commits: GitLabMRCommit[] +} + +// danger.gitlab +interface GitLabDSL extends GitLabJSONDSL { + api: GitLabAPI + utils: { + fileContents(path: string, repoSlug?: string, ref?: string): Promise + } +} + +// --- +// JSON responses from API + +interface GitLabUser { + id: number + name: string + username: string + state: "active" // XXX: other states? + avatar_url: string | null + web_url: string +} + +interface GitLabUserProfile extends GitLabUser { + created_at: string + bio: string | null + location: string | null + public_email: string + skype: string + linkedin: string + twitter: string + website_url: string + organization: string + last_sign_in_at: string + confirmed_at: string + theme_id: number + last_activity_on: string + color_scheme_id: number + projects_limit: number + current_sign_in_at: string + identities: [{ provider: string; extern_uid: string }] + can_create_group: boolean + can_create_project: boolean + two_factor_enabled: boolean + external: boolean + private_profile: boolean +} + +interface GitLabMRBase { + /** */ + id: number + + /** */ + iid: number + + /** */ + project_id: number + + /** */ + title: string + + /** */ + description: string + + /** */ + state: "closed" | "open" | "locked" | "merged" + + /** */ + created_at: string + + /** */ + updated_at: string + + target_branch: string + source_branch: string + upvotes: number + downvotes: number + + author: GitLabUser + user: { + can_merge: boolean + } + assignee: GitLabUser + source_project_id: number + target_project_id: number + labels: string[] + work_in_progress: boolean + milestone: { + id: number + iid: number + project_id: number + title: string + description: string + state: "closed" // XXX: other states? + created_at: string + updated_at: string + due_date: string + start_date: string + web_url: string + } + merge_when_pipeline_succeeds: boolean + merge_status: "can_be_merged" // XXX: other statuses? + merge_error: null | null + sha: string + merge_commit_sha: string | null + user_notes_count: number + discussion_locked: null | null + should_remove_source_branch: boolean + force_remove_source_branch: boolean + allow_collaboration: boolean + allow_maintainer_to_push: boolean + web_url: string + time_stats: { + time_estimate: number + total_time_spent: number + human_time_estimate: number | null + human_total_time_spent: number | null + } +} + +interface GitLabMR extends GitLabMRBase { + squash: boolean + subscribed: boolean + changes_count: string + merged_by: GitLabUser + merged_at: string + closed_by: GitLabUser | null + closed_at: string | null + latest_build_started_at: string + latest_build_finished_at: string + first_deployed_to_production_at: string | null + pipeline: { + id: number + sha: string + ref: string + status: "success" // XXX: other statuses? + web_url: string + } + diff_refs: { + base_sha: string + head_sha: string + start_sha: string + } + diverged_commits_count: number + rebase_in_progress: boolean + approvals_before_merge: null | null +} + +interface GitLabMRChange { + old_path: string + new_path: string + a_mode: string + b_mode: string + diff: string + new_file: boolean + renamed_file: boolean + deleted_file: boolean +} + +interface GitLabMRChanges extends GitLabMRBase { + changes: GitLabMRChange[] +} + +interface GitLabNote { + id: number + type: "DiffNote" | "DiscussionNote" | null // XXX: other types? null means "normal comment" + body: string + attachment: null // XXX: what can an attachment be? + author: GitLabUser + created_at: string + updated_at: string + system: boolean + noteable_id: number + noteable_type: "MergeRequest" // XXX: other types...? + resolvable: boolean + noteable_iid: number +} + +interface GitLabDiscussionTextPosition { + position_type: "text" + base_sha: string + start_sha: string + head_sha: string + new_path: string + new_line: number + old_path: string + old_line: number | null +} + +interface GitLabInlineNote extends GitLabNote { + position: { + base_sha: string + start_sha: string + head_sha: string + old_path: string + new_path: string + position_type: "text" // XXX: other types? + old_line: number | null + new_line: number + } + resolvable: boolean + resolved: boolean + resolved_by: GitLabUser | null +} + +interface GitLabMRCommit { + id: string + short_id: string + created_at: string + parent_ids: string[] + title: string + message: string + author_name: string + author_email: string + authored_date: string + committer_name: string + committer_email: string + committed_date: string +} + /** * The result of user doing warn, message or fail, built this way for * expansion later. @@ -1331,4 +1569,5 @@ export declare type MatchResult = _MatchResult & { /** Returns an object containing arrays of matched files instead of the usual boolean values. */ getKeyedPaths(): KeyedPaths } +/** A vendored copy of the Chainsmoker module on NPM */ export declare type Chainsmoker = (...patterns: Pattern[]) => MatchResult diff --git a/source/dsl/DangerDSL.ts b/source/dsl/DangerDSL.ts index 732d3076e..86ede370c 100644 --- a/source/dsl/DangerDSL.ts +++ b/source/dsl/DangerDSL.ts @@ -5,6 +5,7 @@ import { GitHubDSL } from "../dsl/GitHubDSL" import { BitBucketServerDSL, BitBucketServerJSONDSL } from "../dsl/BitBucketServerDSL" import { DangerUtilsDSL } from "./DangerUtilsDSL" import { CliArgs } from "../dsl/cli-args" +import { GitLabDSL } from "./GitLabDSL" /** * The shape of the JSON passed between Danger and a subprocess. It's built @@ -57,6 +58,8 @@ export interface DangerDSLJSONType { github?: GitHubDSL /** The data only version of BitBucket Server DSL */ bitbucket_server?: BitBucketServerJSONDSL + /** The data only version of GitLab DSL */ + gitlab?: GitLabDSL /** * Used in the Danger JSON DSL to pass metadata between * processes. It will be undefined when used inside the Danger DSL @@ -119,12 +122,23 @@ export interface DangerDSLType { * comments and reviews on the PR, related issues, commits, comments * and activities. * - * Strictly speaking, `bitbucket_server` is a nullable type, if you are using - * GitHub then it will be undefined. For the DSL convenience sake though, it + * Strictly speaking, `bitbucket_server` is a nullable type, if you are not using + * BitBucket Server then it will be undefined. For the DSL convenience sake though, it * is classed as non-nullable */ readonly bitbucket_server: BitBucketServerDSL + /** + * The GitLab metadata. This covers things like PR info, + * comments and reviews on the MR, commits, comments + * and activities. + * + * Strictly speaking, `gitlab` is a nullable type, if you are not using + * GitLab then it will be undefined. For the DSL convenience sake though, it + * is classed as non-nullable + */ + readonly gitlab: GitLabDSL + /** * Functions which are globally useful in most Dangerfiles. Right * now, these functions are around making sentences of arrays, or @@ -138,6 +152,7 @@ export interface DangerDSLType { export class DangerDSL { public readonly github?: GitHubDSL public readonly bitbucket_server?: BitBucketServerDSL + public readonly gitlab?: GitLabDSL constructor(platformDSL: any, public readonly git: GitJSONDSL, public readonly utils: DangerUtilsDSL, name: string) { switch (name) { @@ -146,6 +161,8 @@ export class DangerDSL { this.github = platformDSL case "BitBucketServer": this.bitbucket_server = platformDSL + case "GitLab": + this.gitlab = platformDSL } } } diff --git a/source/dsl/GitLabDSL.ts b/source/dsl/GitLabDSL.ts new file mode 100644 index 000000000..59b804f81 --- /dev/null +++ b/source/dsl/GitLabDSL.ts @@ -0,0 +1,225 @@ +// TODO: extract out from BitBucket specifically, or create our own type +import { RepoMetaData } from "./BitBucketServerDSL" + +// getPlatformReviewDSLRepresentation +export interface GitLabJSONDSL { + metadata: RepoMetaData + mr: GitLabMR + commits: GitLabMRCommit[] +} + +// danger.gitlab +export interface GitLabDSL extends GitLabJSONDSL { + utils: { + fileContents(path: string, repoSlug?: string, ref?: string): Promise + } +} + +// --- +// JSON responses from API + +export interface GitLabUser { + id: number + name: string + username: string + state: "active" // XXX: other states? + avatar_url: string | null + web_url: string +} + +export interface GitLabUserProfile extends GitLabUser { + created_at: string + bio: string | null + location: string | null + public_email: string + skype: string + linkedin: string + twitter: string + website_url: string + organization: string + last_sign_in_at: string + confirmed_at: string + theme_id: number + last_activity_on: string + color_scheme_id: number + projects_limit: number + current_sign_in_at: string + identities: [{ provider: string; extern_uid: string }] + can_create_group: boolean + can_create_project: boolean + two_factor_enabled: boolean + external: boolean + private_profile: boolean +} + +export interface GitLabMRBase { + /** */ + id: number + + /** */ + iid: number + + /** */ + project_id: number + + /** */ + title: string + + /** */ + description: string + + /** */ + state: "closed" | "open" | "locked" | "merged" + + /** */ + created_at: string + + /** */ + updated_at: string + + target_branch: string + source_branch: string + upvotes: number + downvotes: number + + author: GitLabUser + user: { + can_merge: boolean + } + assignee: GitLabUser + source_project_id: number + target_project_id: number + labels: string[] + work_in_progress: boolean + milestone: { + id: number + iid: number + project_id: number + title: string + description: string + state: "closed" // XXX: other states? + created_at: string + updated_at: string + due_date: string + start_date: string + web_url: string + } + merge_when_pipeline_succeeds: boolean + merge_status: "can_be_merged" // XXX: other statuses? + merge_error: null | null + sha: string + merge_commit_sha: string | null + user_notes_count: number + discussion_locked: null | null + should_remove_source_branch: boolean + force_remove_source_branch: boolean + allow_collaboration: boolean + allow_maintainer_to_push: boolean + web_url: string + time_stats: { + time_estimate: number + total_time_spent: number + human_time_estimate: number | null + human_total_time_spent: number | null + } +} + +export interface GitLabMR extends GitLabMRBase { + squash: boolean + subscribed: boolean + changes_count: string + merged_by: GitLabUser + merged_at: string + closed_by: GitLabUser | null + closed_at: string | null + latest_build_started_at: string + latest_build_finished_at: string + first_deployed_to_production_at: string | null + pipeline: { + id: number + sha: string + ref: string + status: "success" // XXX: other statuses? + web_url: string + } + diff_refs: { + base_sha: string + head_sha: string + start_sha: string + } + diverged_commits_count: number + rebase_in_progress: boolean + approvals_before_merge: null | null +} + +export interface GitLabMRChange { + old_path: string + new_path: string + a_mode: string + b_mode: string + diff: string + new_file: boolean + renamed_file: boolean + deleted_file: boolean +} + +export interface GitLabMRChanges extends GitLabMRBase { + changes: GitLabMRChange[] +} + +export interface GitLabNote { + id: number + type: "DiffNote" | "DiscussionNote" | null // XXX: other types? null means "normal comment" + body: string + attachment: null // XXX: what can an attachment be? + author: GitLabUser + created_at: string + updated_at: string + system: boolean + noteable_id: number + noteable_type: "MergeRequest" // XXX: other types...? + resolvable: boolean + noteable_iid: number +} + +export interface GitLabDiscussionTextPosition { + position_type: "text" + base_sha: string + start_sha: string + head_sha: string + new_path: string + new_line: number + old_path: string + old_line: number | null +} + +export interface GitLabInlineNote extends GitLabNote { + position: { + base_sha: string + start_sha: string + head_sha: string + old_path: string + new_path: string + position_type: "text" // XXX: other types? + old_line: number | null + new_line: number + } + resolvable: boolean + resolved: boolean + resolved_by: GitLabUser | null +} + +export interface GitLabMRCommit { + id: string + short_id: string + created_at: string + parent_ids: string[] + title: string + message: string + author_name: string + author_email: string + authored_date: string + committer_name: string + committer_email: string + committed_date: string +} diff --git a/source/platforms/GitHub.ts b/source/platforms/GitHub.ts index 9f85cfeb5..4423e1dca 100644 --- a/source/platforms/GitHub.ts +++ b/source/platforms/GitHub.ts @@ -13,7 +13,7 @@ import { GitHubChecksCommenter } from "./github/comms/checksCommenter" export type GitHubType = Platform & { api: GitHubAPI } -export const GitHub = (api: GitHubAPI) => { +export function GitHub(api: GitHubAPI) { /** * Converts the PR JSON into something easily used by the Github API client. */ diff --git a/source/platforms/GitLab.ts b/source/platforms/GitLab.ts new file mode 100644 index 000000000..b42302aa2 --- /dev/null +++ b/source/platforms/GitLab.ts @@ -0,0 +1,195 @@ +import GitLabAPI from "./gitlab/GitLabAPI" +import { Platform, Comment } from "./platform" +import { GitDSL, GitJSONDSL } from "../dsl/GitDSL" +import { GitCommit } from "../dsl/Commit" +import { GitLabDSL, GitLabJSONDSL, GitLabNote } from "../dsl/GitLabDSL" + +import { debug } from "../debug" +import { dangerIDToString } from "../runner/templates/githubIssueTemplate" +const d = debug("GitLab") + +class GitLab implements Platform { + public readonly name: string + + constructor(public readonly api: GitLabAPI) { + this.name = "GitLab" + } + + getReviewInfo = async (): Promise => { + return this.api.getMergeRequestInfo() + } + + // returns the `danger.gitlab` object + getPlatformReviewDSLRepresentation = async (): Promise => { + const mr = await this.getReviewInfo() + const commits = await this.api.getMergeRequestCommits() + return { + metadata: this.api.repoMetadata, + mr, + commits, + } + } + + getPlatformGitRepresentation = async (): Promise => { + const changes = await this.api.getMergeRequestChanges() + const commits = await this.api.getMergeRequestCommits() + + const mappedCommits: GitCommit[] = commits.map(commit => { + return { + sha: commit.id, + author: { + name: commit.author_name, + email: commit.author_email, + date: commit.authored_date, + }, + committer: { + name: commit.committer_name, + email: commit.committer_email, + date: commit.committed_date, + }, + message: commit.message, + parents: commit.parent_ids, + url: `${this.api.projectURL}/commit/${commit.id}`, + tree: null, + } + }) + + // XXX: does "renamed_file"/move count is "delete/create", or "modified"? + const modified_files: string[] = changes + .filter(change => !change.new_file && !change.deleted_file) + .map(change => change.new_path) + const created_files: string[] = changes.filter(change => change.new_file).map(change => change.new_path) + const deleted_files: string[] = changes.filter(change => change.deleted_file).map(change => change.new_path) + + return { + modified_files, + created_files, + deleted_files, + commits: mappedCommits, + } + } + + getInlineComments = async (dangerID: string): Promise => { + const dangerUserID = (await this.api.getUser()).id + + return (await this.api.getMergeRequestInlineNotes()).map(note => { + return { + id: `${note.id}`, + body: note.body, + // XXX: we should re-use the logic in getDangerNotes, need to check what inline comment template we're using if any + ownedByDanger: note.author.id === dangerUserID && note.body.includes(dangerID), + } + }) + } + + supportsCommenting() { + return true + } + + supportsInlineComments() { + return true + } + + updateOrCreateComment = async (dangerID: string, newComment: string): Promise => { + d("updateOrCreateComment", { dangerID, newComment }) + + const notes: GitLabNote[] = await this.getDangerNotes(dangerID) + debugger + + let note: GitLabNote + + if (notes.length) { + // update the first + note = await this.api.updateMergeRequestNote(notes[0].id, newComment) + + // delete the rest + for (let deleteme of notes) { + if (deleteme === notes[0]) { + continue + } + + await this.api.deleteMergeRequestNote(deleteme.id) + } + } else { + // create a new note + note = await this.api.createMergeRequestNote(newComment) + } + + // create URL from note + // "https://gitlab.com/group/project/merge_requests/154#note_132143425" + return `${this.api.mergeRequestURL}#note_${note.id}` + } + + createComment = async (comment: string): Promise => { + d("createComment", { comment }) + return this.api.createMergeRequestNote(comment) + } + + createInlineComment = async (git: GitDSL, comment: string, path: string, line: number): Promise => { + d("createInlineComment", { git, comment, path, line }) + + const mr = await this.api.getMergeRequestInfo() + + return this.api.createMergeRequestDiscussion(comment, { + position_type: "text", + base_sha: mr.diff_refs.base_sha, + start_sha: mr.diff_refs.start_sha, + head_sha: mr.diff_refs.head_sha, + old_path: path, + old_line: null, + new_path: path, + new_line: line, + }) + } + + updateInlineComment = async (comment: string, id: string): Promise => { + d("updateInlineComment", { comment, id }) + const nid = parseInt(id) // fingers crossed + return await this.api.updateMergeRequestNote(nid, comment) + } + + deleteInlineComment = async (id: string): Promise => { + d("deleteInlineComment", { id }) + const nid = parseInt(id) // fingers crossed + return await this.api.deleteMergeRequestNote(nid) + } + + deleteMainComment = async (dangerID: string): Promise => { + const notes = await this.getDangerNotes(dangerID) + for (let note of notes) { + d("deleteMainComment", { id: note.id }) + await this.api.deleteMergeRequestNote(note.id) + } + + return notes.length > 0 + } + + getDangerNotes = async (dangerID: string): Promise => { + const { id: dangerUserId } = await this.api.getUser() + const notes: GitLabNote[] = await this.api.getMergeRequestNotes() + + return notes.filter( + ({ author: { id }, body, system, type }: GitLabNote) => + !system && // system notes are generated when the user interacts with the UI e.g. changing a PR title + type == null && // we only want "normal" comments on the main body of the MR; + id === dangerUserId && + body!.includes(dangerIDToString(dangerID)) // danger-id-(dangerID) is included in a hidden comment in the githubIssueTemplate + ) + } + + updateStatus = async (): Promise => { + d("updateStatus", {}) + return true + } + + getFileContents = this.api.getFileContents +} + +export default GitLab + +export const gitlabJSONToGitLabDSL = (gl: GitLabDSL, api: GitLabAPI): GitLabDSL => ({ + ...gl, + utils: { + fileContents: api.getFileContents, + }, +}) diff --git a/source/platforms/_tests/_pull_request_parser.test.ts b/source/platforms/_tests/_pull_request_parser.test.ts index 3c04edce7..18871b0ec 100644 --- a/source/platforms/_tests/_pull_request_parser.test.ts +++ b/source/platforms/_tests/_pull_request_parser.test.ts @@ -9,6 +9,7 @@ describe("parsing urls", () => { expect(pullRequestParser("https://github.com/facebook/jest/pull/2555")).toEqual({ pullRequestNumber: "2555", repo: "facebook/jest", + platform: "GitHub", }) }) @@ -17,6 +18,7 @@ describe("parsing urls", () => { expect(pullRequestParser(longPR)).toEqual({ pullRequestNumber: "406", repo: "artsy/emission", + platform: "GitHub", }) }) @@ -24,6 +26,7 @@ describe("parsing urls", () => { expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1")).toEqual({ pullRequestNumber: "1", repo: "projects/PROJ/repos/repo", + platform: "BitBucketServer", }) }) @@ -31,6 +34,7 @@ describe("parsing urls", () => { expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/repo/pull-requests/1/overview")).toEqual({ pullRequestNumber: "1", repo: "projects/PROJ/repos/repo", + platform: "BitBucketServer", }) }) @@ -38,6 +42,7 @@ describe("parsing urls", () => { expect(pullRequestParser("http://localhost:7990/projects/PROJ/repos/super-repo/pull-requests/1/overview")).toEqual({ pullRequestNumber: "1", repo: "projects/PROJ/repos/super-repo", + platform: "BitBucketServer", }) }) @@ -47,6 +52,61 @@ describe("parsing urls", () => { ).toEqual({ pullRequestNumber: "1", repo: "projects/PROJ/repos/super-strong.repo_name", + platform: "BitBucketServer", + }) + }) + + describe("GitLab", () => { + describe(".com", () => { + it("handles PRs", () => { + expect(pullRequestParser("https://gitlab.com/GROUP/PROJ/merge_requests/123")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/PROJ", + platform: "GitLab", + }) + }) + + it("handles PRs sub-pages", () => { + expect(pullRequestParser("https://gitlab.com/GROUP/PROJ/merge_requests/123/commits")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/PROJ", + platform: "GitLab", + }) + }) + + it("handles sub-groups", () => { + expect(pullRequestParser("https://gitlab.com/GROUP/SUBGROUP/PROJ/merge_requests/123")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/SUBGROUP/PROJ", + platform: "GitLab", + }) + }) + }) + + describe("CE/EE", () => { + it("handles PRs", () => { + expect(pullRequestParser("https://localdomain/GROUP/PROJ/merge_requests/123")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/PROJ", + platform: "GitLab", + }) + }) + + it("handles PRs sub-pages", () => { + expect(pullRequestParser("https://localdomain/GROUP/PROJ/merge_requests/123/commits")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/PROJ", + platform: "GitLab", + }) + }) + + it("handles sub-groups", () => { + expect(pullRequestParser("https://localdomain/GROUP/SUBGROUP/PROJ/merge_requests/123")).toEqual({ + pullRequestNumber: "123", + repo: "GROUP/SUBGROUP/PROJ", + platform: "GitLab", + }) + }) }) }) }) diff --git a/source/platforms/_tests/fixtures/gitlab_mr.json b/source/platforms/_tests/fixtures/gitlab_mr.json new file mode 100644 index 000000000..caf9540fe --- /dev/null +++ b/source/platforms/_tests/fixtures/gitlab_mr.json @@ -0,0 +1,100 @@ +{ + "id": 1, + "iid": 1, + "project_id": 3, + "title": "test1", + "description": "fixed login page css paddings", + "state": "merged", + "created_at": "2017-04-29T08:46:00Z", + "updated_at": "2017-04-29T08:46:00Z", + "target_branch": "master", + "source_branch": "test1", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 1, + "name": "Administrator", + "username": "admin", + "state": "active", + "avatar_url": null, + "web_url": "https://gitlab.example.com/admin" + }, + "user": { + "can_merge": false + }, + "assignee": { + "id": 1, + "name": "Administrator", + "username": "admin", + "state": "active", + "avatar_url": null, + "web_url": "https://gitlab.example.com/admin" + }, + "source_project_id": 2, + "target_project_id": 3, + "labels": ["Community contribution", "Manage"], + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 3, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": "2018-09-22", + "start_date": "2018-08-08", + "web_url": "https://gitlab.example.com/my-group/my-project/milestones/1" + }, + "merge_when_pipeline_succeeds": true, + "merge_status": "can_be_merged", + "merge_error": null, + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": null, + "user_notes_count": 1, + "discussion_locked": null, + "should_remove_source_branch": true, + "force_remove_source_branch": false, + "allow_collaboration": false, + "allow_maintainer_to_push": false, + "web_url": "http://gitlab.example.com/my-group/my-project/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "squash": false, + "subscribed": false, + "changes_count": "1", + "merged_by": { + "id": 87854, + "name": "Douwe Maan", + "username": "DouweM", + "state": "active", + "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", + "web_url": "https://gitlab.com/DouweM" + }, + "merged_at": "2018-09-07T11:16:17.520Z", + "closed_by": null, + "closed_at": null, + "latest_build_started_at": "2018-09-07T07:27:38.472Z", + "latest_build_finished_at": "2018-09-07T08:07:06.012Z", + "first_deployed_to_production_at": null, + "pipeline": { + "id": 29626725, + "sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f", + "ref": "patch-28", + "status": "success", + "web_url": "https://gitlab.example.com/my-group/my-project/pipelines/29626725" + }, + "diff_refs": { + "base_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00", + "head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f", + "start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00" + }, + "diverged_commits_count": 2, + "rebase_in_progress": false, + "approvals_before_merge": null +} diff --git a/source/platforms/gitlab/GitLabAPI.ts b/source/platforms/gitlab/GitLabAPI.ts new file mode 100644 index 000000000..eaba34c58 --- /dev/null +++ b/source/platforms/gitlab/GitLabAPI.ts @@ -0,0 +1,206 @@ +import { RepoMetaData } from "../../dsl/BitBucketServerDSL" +import { api as fetch } from "../../api/fetch" +import { + GitLabDiscussionTextPosition, + GitLabInlineNote, + GitLabMR, + GitLabMRChange, + GitLabMRChanges, + GitLabMRCommit, + GitLabNote, + GitLabUserProfile, +} from "../../dsl/GitLabDSL" + +import { Gitlab } from "gitlab" +import { Env } from "../../ci_source/ci_source" +import { debug } from "../../debug" + +export type GitLabAPIToken = string + +export interface GitLabAPICredentials { + host: string + token: GitLabAPIToken +} + +export function getGitLabAPICredentialsFromEnv(env: Env): GitLabAPICredentials { + let host = "https://gitlab.com" + const envHost = env["DANGER_GITLAB_HOST"] + if (envHost) { + // We used to support DANGER_GITLAB_HOST being just the host e.g. "gitlab.com" + // however it is possible to have a custom host without SSL, ensure we only add the protocol if one is not provided + const protocolRegex = /^http(s)*?:\/\//i + host = protocolRegex.test(envHost) ? envHost : `https://${envHost}` + } + + return { + host, + token: env["DANGER_GITLAB_API_TOKEN"], + } +} + +class GitLabAPI { + fetch: typeof fetch + private api: any + private readonly hostURL: string + private readonly d = debug("GitLabAPI") + + constructor(public readonly repoMetadata: RepoMetaData, public readonly repoCredentials: GitLabAPICredentials) { + this.fetch = fetch + this.api = new Gitlab(repoCredentials) + this.hostURL = repoCredentials.host + } + + get projectURL(): string { + return `${this.hostURL}/${this.repoMetadata.repoSlug}` + } + + get mergeRequestURL(): string { + return `${this.projectURL}/merge_requests/${this.repoMetadata.pullRequestID}` + } + + getUser = async (): Promise => { + this.d("getUser") + const user: GitLabUserProfile = await this.api.Users.current() + this.d("getUser", user) + return user + } + + getMergeRequestInfo = async (): Promise => { + this.d(`getMergeRequestInfo for repo: ${this.repoMetadata.repoSlug} pr: ${this.repoMetadata.pullRequestID}`) + const mr: GitLabMR = await this.api.MergeRequests.show(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + this.d("getMergeRequestInfo", mr) + return mr + } + + getMergeRequestChanges = async (): Promise => { + this.d(`getMergeRequestChanges for repo: ${this.repoMetadata.repoSlug} pr: ${this.repoMetadata.pullRequestID}`) + const mr = (await this.api.MergeRequests.changes( + this.repoMetadata.repoSlug, + this.repoMetadata.pullRequestID + )) as GitLabMRChanges + + this.d("getMergeRequestChanges", mr.changes) + return mr.changes + } + + getMergeRequestCommits = async (): Promise => { + this.d("getMergeRequestCommits", this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + const commits: GitLabMRCommit[] = await this.api.MergeRequests.commits( + this.repoMetadata.repoSlug, + this.repoMetadata.pullRequestID + ) + this.d("getMergeRequestCommits", commits) + return commits + } + + getMergeRequestNotes = async (): Promise => { + this.d("getMergeRequestNotes", this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + const api = this.api.MergeRequestNotes + const notes: GitLabNote[] = await api.all(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + this.d("getMergeRequestNotes", notes) + return notes + } + + getMergeRequestInlineNotes = async (): Promise => { + this.d("getMergeRequestInlineNotes") + const notes: GitLabNote[] = await this.getMergeRequestNotes() + const inlineNotes = notes.filter((note: GitLabNote) => note.type == "DiffNote") as GitLabInlineNote[] + this.d("getMergeRequestInlineNotes", inlineNotes) + return inlineNotes + } + + createMergeRequestDiscussion = async (content: string, position: GitLabDiscussionTextPosition): Promise => { + this.d( + "createMergeRequestDiscussion", + this.repoMetadata.repoSlug, + this.repoMetadata.pullRequestID, + content, + position + ) + const api = this.api.MergeRequestDiscussions + + try { + const result: string = await api.create(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, content, { + position: position, + }) + this.d("createMergeRequestDiscussion", result) + return result + } catch (e) { + this.d("createMergeRequestDiscussion", e) + throw e + } + } + + createMergeRequestNote = async (body: string): Promise => { + this.d("createMergeRequestNote", this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, body) + const api = this.api.MergeRequestNotes + + try { + this.d("createMergeRequestNote") + const note: GitLabNote = await api.create(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, body) + this.d("createMergeRequestNote", note) + return note + } catch (e) { + this.d("createMergeRequestNote", e) + } + + return Promise.reject() + } + + updateMergeRequestNote = async (id: number, body: string): Promise => { + this.d("updateMergeRequestNote", this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, id, body) + const api = this.api.MergeRequestNotes + try { + const note: GitLabNote = await api.edit(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, id, body) + this.d("updateMergeRequestNote", note) + return note + } catch (e) { + this.d("updateMergeRequestNote", e) + } + + return Promise.reject() + } + + // note: deleting the _only_ note in a discussion also deletes the discussion \o/ + deleteMergeRequestNote = async (id: number): Promise => { + this.d("deleteMergeRequestNote", this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, id) + const api = this.api.MergeRequestNotes + + try { + await api.remove(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID, id) + this.d("deleteMergeRequestNote", true) + return true + } catch (e) { + this.d("deleteMergeRequestNote", e) + return false + } + } + + getFileContents = async (path: string, slug?: string, ref?: string): Promise => { + this.d(`getFileContents requested for path:${path}, slug:${slug}, ref:${ref}`) + const api = this.api.RepositoryFiles + const projectId = slug || this.repoMetadata.repoSlug + // Use the current state of PR if no ref is passed + if (!ref) { + const mr: GitLabMR = await this.getMergeRequestInfo() + ref = mr.diff_refs.head_sha + } + + try { + this.d("getFileContents", projectId, path, ref) + const response = await api.show(projectId, path, ref) + const result: string = Buffer.from(response.content, "base64").toString() + this.d("getFileContents", result) + return result + } catch (e) { + this.d("getFileContents", e) + // GitHubAPI.fileContents returns "" when the file does not exist, keep it consistent across providers + if (e.response.status === 404) { + return "" + } + throw e + } + } +} + +export default GitLabAPI diff --git a/source/platforms/gitlab/GitLabGit.ts b/source/platforms/gitlab/GitLabGit.ts new file mode 100644 index 000000000..33e35871e --- /dev/null +++ b/source/platforms/gitlab/GitLabGit.ts @@ -0,0 +1,26 @@ +import { debug } from "../../debug" +import { GitLabDSL } from "../../dsl/GitLabDSL" +import { GitDSL, GitJSONDSL } from "../../dsl/GitDSL" +import { gitJSONToGitDSL, GitJSONToGitDSLConfig, GitStructuredDiff } from "../git/gitJSONToGitDSL" + +const d = debug("GitLabGit") + +export const gitLabGitDSL = (gitlab: GitLabDSL, json: GitJSONDSL): GitDSL => { + const config: GitJSONToGitDSLConfig = { + repo: `${gitlab.mr.project_id}`, // we don't get the repo slug, but `project_id` is equivalent in API calls + baseSHA: gitlab.mr.diff_refs.base_sha, + headSHA: gitlab.mr.diff_refs.head_sha, + getFileContents: gitlab.utils.fileContents, + // TODO: implement me when the API methods are in + getFullDiff: async (): Promise => { + throw new Error("getFullDiff is not yet implemented") + }, + // TODO: implement me when the API methods are in + getStructuredDiffForFile: async (): Promise => { + throw new Error("getStructuredDiffForFile is not yet implemented") + }, + } + + d("Setting up git DSL with: ", config) + return gitJSONToGitDSL(json, config) +} diff --git a/source/platforms/gitlab/_tests/_gitlab_api.test.ts b/source/platforms/gitlab/_tests/_gitlab_api.test.ts new file mode 100644 index 000000000..05f3bb0e1 --- /dev/null +++ b/source/platforms/gitlab/_tests/_gitlab_api.test.ts @@ -0,0 +1,129 @@ +import nock, { NockDefinition } from "nock" +import { default as GitLabAPI, getGitLabAPICredentialsFromEnv } from "../GitLabAPI" +import { resolve } from "path" +import { readFileSync } from "fs" + +const nockBack = nock.back +nockBack.fixtures = __dirname + "/fixtures" + +// We're testing https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27117 +// This has been chosen because it is already merged and publicly available, it's unlikely to change + +/** Returns a fixture. */ +const loadFixture = (path: string): any => + JSON.parse(readFileSync(resolve(nockBack.fixtures, `${path}.json`), {}).toString())[0] + +describe("GitLab API", () => { + let api: GitLabAPI + + beforeAll(() => { + nock.recorder.rec() + nockBack.setMode("record") + }) + + afterAll(() => { + nock.restore() + }) + + beforeEach(() => { + api = new GitLabAPI( + { pullRequestID: "27117", repoSlug: "gitlab-org/gitlab-ce" }, + getGitLabAPICredentialsFromEnv({ + DANGER_GITLAB_HOST: "gitlab.com", + DANGER_GITLAB_API_TOKEN: "FAKE_DANGER_GITLAB_API_TOKEN", + }) + ) + }) + + it("projectURL is defined", () => { + expect(api.projectURL).toBe("https://gitlab.com/gitlab-org/gitlab-ce") + }) + + it("mergeRequestURL is defined", () => { + expect(api.mergeRequestURL).toBe("https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27117") + }) + + const sanitizeUserResponse = (nocks: NockDefinition[]): NockDefinition[] => { + return nocks.map((nock: NockDefinition) => { + let { response, ...restNock } = nock + + // @ts-ignore + const { identities } = response + + response = { + // @ts-ignore + ...response, + username: "username", + name: "First Last", + organization: "My Organization", + email: "username@example.com", + avatar_url: "https://www.", + web_url: "https://www.", + identities: identities.map(({ extern_uid, ...rest }: any) => ({ ...rest, extern_uid: "xxxx" })), + } + + return { ...restNock, response } + }) + } + + it("getUser returns the current user profile id", async () => { + // To re-record this you need to provide a valid DANGER_GITLAB_API_TOKEN + + const { nockDone } = await nockBack("getUser.json", { afterRecord: sanitizeUserResponse }) + const result = await api.getUser() + nockDone() + const { response } = loadFixture("getUser") + expect(result).toEqual(response) + }) + + it("getMergeRequestInfo", async () => { + const { nockDone } = await nockBack("getMergeRequestInfo.json") + const result = await api.getMergeRequestInfo() + nockDone() + const { response } = loadFixture("getMergeRequestInfo") + expect(result).toEqual(response) + }) + + it("getMergeRequestChanges", async () => { + const { nockDone } = await nockBack("getMergeRequestChanges.json") + const result = await api.getMergeRequestChanges() + nockDone() + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + old_path: expect.any(String), + new_path: expect.any(String), + a_mode: expect.any(String), + b_mode: expect.any(String), + diff: expect.any(String), + new_file: expect.any(Boolean), + deleted_file: expect.any(Boolean), + }), + ]) + ) + }) + + it("getMergeRequestCommits", async () => { + const { nockDone } = await nockBack("getMergeRequestCommits.json") + const result = await api.getMergeRequestCommits() + nockDone() + const { response } = loadFixture("getMergeRequestCommits") + expect(result).toEqual(response) + }) + + it("getMergeRequestNotes", async () => { + const { nockDone } = await nockBack("getMergeRequestNotes.json") + const result = await api.getMergeRequestNotes() + nockDone() + const { response } = loadFixture("getMergeRequestNotes") + expect(result).toEqual(response) + }) + + it("getMergeRequestInlineNotes", async () => { + const { nockDone } = await nockBack("getMergeRequestInlineNotes.json") + const result = await api.getMergeRequestInlineNotes() + nockDone() + // TODO: There are no inline notes on this MR, we should look for a public one that has inline notes to improve this test + expect(result).toEqual([]) + }) +}) diff --git a/source/platforms/gitlab/_tests/fixtures/getMergeRequestChanges.json b/source/platforms/gitlab/_tests/fixtures/getMergeRequestChanges.json new file mode 100644 index 000000000..8a378ad5e --- /dev/null +++ b/source/platforms/gitlab/_tests/fixtures/getMergeRequestChanges.json @@ -0,0 +1,204 @@ +[ + { + "scope": "https://gitlab.com:443", + "method": "GET", + "path": "/api/v4/projects/gitlab-org%2Fgitlab-ce/merge_requests/27117/changes", + "body": "", + "status": 200, + "response": { + "id": 27253868, + "iid": 27117, + "project_id": 13083, + "title": "Stable reviewer roulette", + "description": "Change reviewer roulette to always pick the same reviewers for the same\nbranch name. We do this by:\n\n1. Making the branch name 'canonical' across CE and EE by stripping a\n leading 'ce-' or 'ee-' and a trailing '-ce' or '-ee'. If people are\n following our branch naming guidelines, this should give the same\n branch name in both repos.\n2. Converting the branch name to a stable integer by taking the integer\n form of its MD5.\n3. Passing that integer as a seed to Ruby's `Random` class, which 'may\n be used to ensure repeatable sequences of pseudo-random numbers\n between different runs of the program' (from the Ruby documentation).\n\nThe upshot is that the same branch name (in CE and EE) should always\npick the same reviewers, and those should be evenly distributed across\nthe set of possible reviewers due to the use of MD5.\n\nAgain, I have a test script:\n\n```ruby\nrequire 'ffaker'\n\nclass Foo\n include Gitlab::Danger::Helper\nend\n\ndef spin(team, project, category, branch_name)\n # Strip leading and trailing CE/EE markers\n canonical_branch_name = branch_name.gsub(/^[ce]e-/, '').gsub(/-[ce]e$/, '')\n rng = Random.new(Digest::MD5.hexdigest(canonical_branch_name).to_i(16))\n\n reviewers = team.select { |member| member.reviewer?(project, category) }\n traintainers = team.select { |member| member.traintainer?(project, category) }\n maintainers = team.select { |member| member.maintainer?(project, category) }\n\n # TODO: filter out people who are currently not in the office\n # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652\n #\n # TODO: take CODEOWNERS into account?\n # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653\n\n # Make traintainers have triple the chance to be picked as a reviewer\n reviewer = (reviewers + traintainers + traintainers).sample(random: rng)\n maintainer = maintainers.sample(random: rng)\n\n [reviewer.username, maintainer.username]\nend\n\ndef random_branch_name\n FFaker::Filesystem.file_name\nend\n\nFFaker::Random.seed = 123\nteam = Foo.new.project_team\nresults = Hash.new(0)\n\n10_000.times do\n reviewer, maintainer = spin(team, 'gitlab-ce', 'backend', random_branch_name)\n\n results[reviewer] += 1\n results[maintainer] += 1\nend\n\nresults.sort_by(&:last).reverse.each do |username, picked|\n puts \"#{username}: #{picked}\"\nend; nil\n```\n\nThis should output the same for you as it does for me, because we seed the branch names too!\n\n```\ndzaporozhets: 799\nmkozono: 797\ngrzesiek: 794\ngodfat: 793\nDouweM: 788\nnick.thomas: 773\ntkuah: 764\nstanhu: 761\nayufan: 759\njprovaznik: 758\njameslopez: 757\ndbalexandre: 754\nsmcgivern: 751\nsplattael: 744\nashmckenzie: 741\nrspeicher: 739\njarka: 738\nrymai: 735\nreprazent: 729\nmayra-cabrera: 713\nmdelaossa: 273\nrdavila: 269\nvsizov: 260\ntheoretick: 257\noswaldo: 255\nifarkas: 255\nbrytannia: 248\nengwan: 248\nfelipe_artur: 247\nrpereira2: 243\ntoon: 239\nrossfuhrman: 236\njamedjo: 235\nfjsanpedro: 229\nmatteeyah: 226\nbrodock: 226\nvzagorodny: 222\nzj: 220\nmarkglenfletcher: 219\ndosuken123: 206\n```\n\nCloses https://gitlab.com/gitlab-org/gitlab-ce/issues/57766.", + "state": "merged", + "created_at": "2019-04-08T10:59:38.140Z", + "updated_at": "2019-05-02T14:34:54.068Z", + "merged_by": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "merged_at": "2019-04-09T13:57:11.931Z", + "closed_by": null, + "closed_at": null, + "target_branch": "master", + "source_branch": "stable-reviewer-roulette", + "user_notes_count": 4, + "upvotes": 3, + "downvotes": 0, + "assignee": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "assignees": [ + { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + } + ], + "source_project_id": 13083, + "target_project_id": 13083, + "labels": ["Danger bot", "Plan", "backend", "backstage"], + "work_in_progress": false, + "milestone": { + "id": 655280, + "iid": 28, + "group_id": 9970, + "title": "11.11", + "description": "", + "state": "active", + "created_at": "2018-09-21T19:08:37.027Z", + "updated_at": "2019-01-16T19:48:19.411Z", + "due_date": "2019-05-22", + "start_date": "2019-04-08", + "web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/28" + }, + "merge_when_pipeline_succeeds": false, + "merge_status": "can_be_merged", + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "merge_commit_sha": "58d4099c1469dba9ff850733ba29da11f6eeb158", + "discussion_locked": null, + "should_remove_source_branch": null, + "force_remove_source_branch": true, + "reference": "!27117", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27117", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "squash": false, + "subscribed": false, + "changes_count": "1", + "latest_build_started_at": "2019-04-08T10:56:58.249Z", + "latest_build_finished_at": "2019-04-08T11:56:52.871Z", + "first_deployed_to_production_at": null, + "pipeline": { + "id": 55706028, + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "ref": "stable-reviewer-roulette", + "status": "success", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/pipelines/55706028" + }, + "head_pipeline": { + "id": 55706028, + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "ref": "stable-reviewer-roulette", + "status": "success", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/pipelines/55706028", + "before_sha": "0000000000000000000000000000000000000000", + "tag": false, + "yaml_errors": null, + "user": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:56:50.639Z", + "updated_at": "2019-04-08T11:56:52.890Z", + "started_at": "2019-04-08T10:56:58.249Z", + "finished_at": "2019-04-08T11:56:52.871Z", + "committed_at": null, + "duration": 3592, + "coverage": "76.34", + "detailed_status": { + "icon": "status_warning", + "text": "passed", + "label": "passed with warnings", + "group": "success-with-warnings", + "tooltip": "passed", + "has_details": true, + "details_path": "/gitlab-org/gitlab-ce/pipelines/55706028", + "illustration": null, + "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" + } + }, + "diff_refs": { + "base_sha": "50cd5d9b776848cf23f1fd1ec52789dbdf946185", + "head_sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "start_sha": "50cd5d9b776848cf23f1fd1ec52789dbdf946185" + }, + "merge_error": null, + "user": { + "can_merge": false + }, + "changes": [ + { + "old_path": "danger/roulette/Dangerfile", + "new_path": "danger/roulette/Dangerfile", + "a_mode": "100644", + "b_mode": "100644", + "new_file": false, + "renamed_file": false, + "deleted_file": false, + "diff": "@@ -1,5 +1,7 @@\n # frozen_string_literal: true\n \n+require 'digest/md5'\n+\n MESSAGE = <; rel=\"first\", ; rel=\"last\"", + "Vary", + "Origin", + "X-Content-Type-Options", + "nosniff", + "X-Frame-Options", + "SAMEORIGIN", + "X-Next-Page", + "", + "X-Page", + "1", + "X-Per-Page", + "20", + "X-Prev-Page", + "", + "X-Request-Id", + "ddjTNYbzfd4", + "X-Runtime", + "0.063964", + "X-Total", + "2", + "X-Total-Pages", + "1", + "Strict-Transport-Security", + "max-age=31536000", + "Referrer-Policy", + "strict-origin-when-cross-origin", + "RateLimit-Limit", + "600", + "RateLimit-Observed", + "1", + "RateLimit-Remaining", + "599", + "RateLimit-Reset", + "1558351176", + "RateLimit-ResetTime", + "Mon, 20 May 2019 11:19:36 GMT" + ] + } +] diff --git a/source/platforms/gitlab/_tests/fixtures/getMergeRequestInfo.json b/source/platforms/gitlab/_tests/fixtures/getMergeRequestInfo.json new file mode 100644 index 000000000..ae25daf87 --- /dev/null +++ b/source/platforms/gitlab/_tests/fixtures/getMergeRequestInfo.json @@ -0,0 +1,192 @@ +[ + { + "scope": "https://gitlab.com:443", + "method": "GET", + "path": "/api/v4/projects/gitlab-org%2Fgitlab-ce/merge_requests/27117", + "body": "", + "status": 200, + "response": { + "id": 27253868, + "iid": 27117, + "project_id": 13083, + "title": "Stable reviewer roulette", + "description": "Change reviewer roulette to always pick the same reviewers for the same\nbranch name. We do this by:\n\n1. Making the branch name 'canonical' across CE and EE by stripping a\n leading 'ce-' or 'ee-' and a trailing '-ce' or '-ee'. If people are\n following our branch naming guidelines, this should give the same\n branch name in both repos.\n2. Converting the branch name to a stable integer by taking the integer\n form of its MD5.\n3. Passing that integer as a seed to Ruby's `Random` class, which 'may\n be used to ensure repeatable sequences of pseudo-random numbers\n between different runs of the program' (from the Ruby documentation).\n\nThe upshot is that the same branch name (in CE and EE) should always\npick the same reviewers, and those should be evenly distributed across\nthe set of possible reviewers due to the use of MD5.\n\nAgain, I have a test script:\n\n```ruby\nrequire 'ffaker'\n\nclass Foo\n include Gitlab::Danger::Helper\nend\n\ndef spin(team, project, category, branch_name)\n # Strip leading and trailing CE/EE markers\n canonical_branch_name = branch_name.gsub(/^[ce]e-/, '').gsub(/-[ce]e$/, '')\n rng = Random.new(Digest::MD5.hexdigest(canonical_branch_name).to_i(16))\n\n reviewers = team.select { |member| member.reviewer?(project, category) }\n traintainers = team.select { |member| member.traintainer?(project, category) }\n maintainers = team.select { |member| member.maintainer?(project, category) }\n\n # TODO: filter out people who are currently not in the office\n # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652\n #\n # TODO: take CODEOWNERS into account?\n # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653\n\n # Make traintainers have triple the chance to be picked as a reviewer\n reviewer = (reviewers + traintainers + traintainers).sample(random: rng)\n maintainer = maintainers.sample(random: rng)\n\n [reviewer.username, maintainer.username]\nend\n\ndef random_branch_name\n FFaker::Filesystem.file_name\nend\n\nFFaker::Random.seed = 123\nteam = Foo.new.project_team\nresults = Hash.new(0)\n\n10_000.times do\n reviewer, maintainer = spin(team, 'gitlab-ce', 'backend', random_branch_name)\n\n results[reviewer] += 1\n results[maintainer] += 1\nend\n\nresults.sort_by(&:last).reverse.each do |username, picked|\n puts \"#{username}: #{picked}\"\nend; nil\n```\n\nThis should output the same for you as it does for me, because we seed the branch names too!\n\n```\ndzaporozhets: 799\nmkozono: 797\ngrzesiek: 794\ngodfat: 793\nDouweM: 788\nnick.thomas: 773\ntkuah: 764\nstanhu: 761\nayufan: 759\njprovaznik: 758\njameslopez: 757\ndbalexandre: 754\nsmcgivern: 751\nsplattael: 744\nashmckenzie: 741\nrspeicher: 739\njarka: 738\nrymai: 735\nreprazent: 729\nmayra-cabrera: 713\nmdelaossa: 273\nrdavila: 269\nvsizov: 260\ntheoretick: 257\noswaldo: 255\nifarkas: 255\nbrytannia: 248\nengwan: 248\nfelipe_artur: 247\nrpereira2: 243\ntoon: 239\nrossfuhrman: 236\njamedjo: 235\nfjsanpedro: 229\nmatteeyah: 226\nbrodock: 226\nvzagorodny: 222\nzj: 220\nmarkglenfletcher: 219\ndosuken123: 206\n```\n\nCloses https://gitlab.com/gitlab-org/gitlab-ce/issues/57766.", + "state": "merged", + "created_at": "2019-04-08T10:59:38.140Z", + "updated_at": "2019-05-02T14:34:54.068Z", + "merged_by": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "merged_at": "2019-04-09T13:57:11.931Z", + "closed_by": null, + "closed_at": null, + "target_branch": "master", + "source_branch": "stable-reviewer-roulette", + "user_notes_count": 4, + "upvotes": 3, + "downvotes": 0, + "assignee": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "assignees": [ + { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + } + ], + "source_project_id": 13083, + "target_project_id": 13083, + "labels": ["Danger bot", "Plan", "backend", "backstage"], + "work_in_progress": false, + "milestone": { + "id": 655280, + "iid": 28, + "group_id": 9970, + "title": "11.11", + "description": "", + "state": "active", + "created_at": "2018-09-21T19:08:37.027Z", + "updated_at": "2019-01-16T19:48:19.411Z", + "due_date": "2019-05-22", + "start_date": "2019-04-08", + "web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/28" + }, + "merge_when_pipeline_succeeds": false, + "merge_status": "can_be_merged", + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "merge_commit_sha": "58d4099c1469dba9ff850733ba29da11f6eeb158", + "discussion_locked": null, + "should_remove_source_branch": null, + "force_remove_source_branch": true, + "reference": "!27117", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27117", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, + "squash": false, + "subscribed": false, + "changes_count": "1", + "latest_build_started_at": "2019-04-08T10:56:58.249Z", + "latest_build_finished_at": "2019-04-08T11:56:52.871Z", + "first_deployed_to_production_at": null, + "pipeline": { + "id": 55706028, + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "ref": "stable-reviewer-roulette", + "status": "success", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/pipelines/55706028" + }, + "head_pipeline": { + "id": 55706028, + "sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "ref": "stable-reviewer-roulette", + "status": "success", + "web_url": "https://gitlab.com/gitlab-org/gitlab-ce/pipelines/55706028", + "before_sha": "0000000000000000000000000000000000000000", + "tag": false, + "yaml_errors": null, + "user": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:56:50.639Z", + "updated_at": "2019-04-08T11:56:52.890Z", + "started_at": "2019-04-08T10:56:58.249Z", + "finished_at": "2019-04-08T11:56:52.871Z", + "committed_at": null, + "duration": 3592, + "coverage": "76.34", + "detailed_status": { + "icon": "status_warning", + "text": "passed", + "label": "passed with warnings", + "group": "success-with-warnings", + "tooltip": "passed", + "has_details": true, + "details_path": "/gitlab-org/gitlab-ce/pipelines/55706028", + "illustration": null, + "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" + } + }, + "diff_refs": { + "base_sha": "50cd5d9b776848cf23f1fd1ec52789dbdf946185", + "head_sha": "28531ab43666b5fdf37e0a70db3bcbf7d3f92183", + "start_sha": "50cd5d9b776848cf23f1fd1ec52789dbdf946185" + }, + "merge_error": null, + "user": { + "can_merge": false + }, + "approvals_before_merge": 1 + }, + "rawHeaders": [ + "Server", + "nginx", + "Date", + "Mon, 20 May 2019 11:12:29 GMT", + "Content-Type", + "application/json", + "Content-Length", + "7154", + "Connection", + "close", + "Cache-Control", + "max-age=0, private, must-revalidate", + "Etag", + "W/\"e59d8234ca897bca73b643b633227c04\"", + "Vary", + "Origin", + "X-Content-Type-Options", + "nosniff", + "X-Frame-Options", + "SAMEORIGIN", + "X-Request-Id", + "126GhJVQrl9", + "X-Runtime", + "0.243159", + "Strict-Transport-Security", + "max-age=31536000", + "Referrer-Policy", + "strict-origin-when-cross-origin", + "RateLimit-Limit", + "600", + "RateLimit-Observed", + "2", + "RateLimit-Remaining", + "598", + "RateLimit-Reset", + "1558350809", + "RateLimit-ResetTime", + "Mon, 20 May 2019 11:13:29 GMT" + ] + } +] diff --git a/source/platforms/gitlab/_tests/fixtures/getMergeRequestInlineNotes.json b/source/platforms/gitlab/_tests/fixtures/getMergeRequestInlineNotes.json new file mode 100644 index 000000000..07891dd80 --- /dev/null +++ b/source/platforms/gitlab/_tests/fixtures/getMergeRequestInlineNotes.json @@ -0,0 +1,398 @@ +[ + { + "scope": "https://gitlab.com:443", + "method": "GET", + "path": "/api/v4/projects/gitlab-org%2Fgitlab-ce/merge_requests/27117/notes", + "body": "", + "status": 200, + "response": [ + { + "id": 166211215, + "type": null, + "body": "mentioned in issue gitlab-org/release/tasks#778", + "attachment": null, + "author": { + "id": 1786152, + "name": "🤖 GitLab Bot 🤖", + "username": "gitlab-bot", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/1786152/avatar.png", + "web_url": "https://gitlab.com/gitlab-bot" + }, + "created_at": "2019-05-02T14:34:54.054Z", + "updated_at": "2019-05-02T14:34:54.054Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931185, + "type": null, + "body": "merged", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:57:11.941Z", + "updated_at": "2019-04-09T13:57:11.941Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931183, + "type": null, + "body": "mentioned in commit 58d4099c1469dba9ff850733ba29da11f6eeb158", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:57:11.670Z", + "updated_at": "2019-04-09T13:57:11.670Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931064, + "type": null, + "body": "Thanks, @smcgivern! LGTM 👍", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:56:58.560Z", + "updated_at": "2019-04-09T13:56:58.560Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158930861, + "type": null, + "body": "approved this merge request", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:56:37.218Z", + "updated_at": "2019-04-09T13:56:37.218Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704850, + "type": null, + "body": "assigned to @dbalexandre", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:04.248Z", + "updated_at": "2019-04-09T06:46:04.248Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704847, + "type": null, + "body": "The reviewers and maintainers changing on each push was a bit confusing. Thanks for fixing!\n\n\n@dbalexandre Over to you :smile:.", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:04.013Z", + "updated_at": "2019-04-09T06:46:04.013Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704845, + "type": null, + "body": "approved this merge request", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:03.790Z", + "updated_at": "2019-04-09T06:46:03.790Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704643, + "type": null, + "body": "resolved all discussions", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:45:15.102Z", + "updated_at": "2019-04-09T06:45:15.102Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158395805, + "type": null, + "body": "assigned to @reprazent", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:22:26.589Z", + "updated_at": "2019-04-08T11:22:26.589Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158395800, + "type": "DiscussionNote", + "body": "This MR:\n\n1. https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/192544935\n2. https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/192559554\n\nEE MR (https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/10640):\n\n1. https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/192548951\n2. https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/192559927\n\nAll picked the same two reviewers :tada: \n\nWhich means @reprazent again. Sorry about that!", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:22:26.118Z", + "updated_at": "2019-04-08T11:22:26.118Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": true, + "resolved": true, + "resolved_by": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "noteable_iid": 27117 + }, + { + "id": 158392374, + "type": "DiscussionNote", + "body": "\n## Reviewer roulette\n\nChanges that require review have been detected! A merge request is normally\nreviewed by both a reviewer and a maintainer in its primary category (e.g.\n~frontend or ~backend), and by a maintainer in all other categories.\n\n\n\nTo spread load more evenly across eligible reviewers, Danger has randomly picked\na candidate for each review slot. Feel free to override this selection if you\nthink someone else would be better-suited, or the chosen person is unavailable.\n\nOnce you've decided who will review this merge request, mention them as you\nnormally would! Danger does not (yet?) automatically notify them for you.\n\n| Category | Reviewer | Maintainer |\n| -------- | -------- | ---------- |\n| ~backend | [Bob Van Landuyt](https://gitlab.com/reprazent) (`@reprazent`) | [Douglas Barbosa Alexandre](https://gitlab.com/dbalexandre) (`@dbalexandre`) |\n\n

\n Generated by :no_entry_sign: Danger\n

\n", + "attachment": null, + "author": { + "id": 1786152, + "name": "🤖 GitLab Bot 🤖", + "username": "gitlab-bot", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/1786152/avatar.png", + "web_url": "https://gitlab.com/gitlab-bot" + }, + "created_at": "2019-04-08T11:12:37.422Z", + "updated_at": "2019-04-08T11:22:26.212Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": true, + "resolved": true, + "resolved_by": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "noteable_iid": 27117 + }, + { + "id": 158388876, + "type": null, + "body": "mentioned in merge request gitlab-ee!10640", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:02:38.416Z", + "updated_at": "2019-04-08T11:02:38.416Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158387911, + "type": null, + "body": "mentioned in issue #57766", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:59:49.849Z", + "updated_at": "2019-04-08T10:59:49.849Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158387842, + "type": null, + "body": "changed milestone to %\"11.11\"", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:59:38.656Z", + "updated_at": "2019-04-08T10:59:38.656Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + } + ], + "rawHeaders": [ + "Server", + "nginx", + "Date", + "Mon, 20 May 2019 11:19:29 GMT", + "Content-Type", + "application/json", + "Content-Length", + "9682", + "Connection", + "close", + "Cache-Control", + "max-age=0, private, must-revalidate", + "Etag", + "W/\"edab8aad8eea37dd376785c34c7c250f\"", + "Link", + "; rel=\"first\", ; rel=\"last\"", + "Vary", + "Origin", + "X-Content-Type-Options", + "nosniff", + "X-Frame-Options", + "SAMEORIGIN", + "X-Next-Page", + "", + "X-Page", + "1", + "X-Per-Page", + "20", + "X-Prev-Page", + "", + "X-Request-Id", + "ztQpuOSW9n8", + "X-Runtime", + "0.418536", + "X-Total", + "15", + "X-Total-Pages", + "1", + "Strict-Transport-Security", + "max-age=31536000", + "Referrer-Policy", + "strict-origin-when-cross-origin", + "RateLimit-Limit", + "600", + "RateLimit-Observed", + "2", + "RateLimit-Remaining", + "598", + "RateLimit-Reset", + "1558351229", + "RateLimit-ResetTime", + "Mon, 20 May 2019 11:20:29 GMT" + ] + } +] diff --git a/source/platforms/gitlab/_tests/fixtures/getMergeRequestNotes.json b/source/platforms/gitlab/_tests/fixtures/getMergeRequestNotes.json new file mode 100644 index 000000000..e44aa2c3f --- /dev/null +++ b/source/platforms/gitlab/_tests/fixtures/getMergeRequestNotes.json @@ -0,0 +1,398 @@ +[ + { + "scope": "https://gitlab.com:443", + "method": "GET", + "path": "/api/v4/projects/gitlab-org%2Fgitlab-ce/merge_requests/27117/notes", + "body": "", + "status": 200, + "response": [ + { + "id": 166211215, + "type": null, + "body": "mentioned in issue gitlab-org/release/tasks#778", + "attachment": null, + "author": { + "id": 1786152, + "name": "🤖 GitLab Bot 🤖", + "username": "gitlab-bot", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/1786152/avatar.png", + "web_url": "https://gitlab.com/gitlab-bot" + }, + "created_at": "2019-05-02T14:34:54.054Z", + "updated_at": "2019-05-02T14:34:54.054Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931185, + "type": null, + "body": "merged", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:57:11.941Z", + "updated_at": "2019-04-09T13:57:11.941Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931183, + "type": null, + "body": "mentioned in commit 58d4099c1469dba9ff850733ba29da11f6eeb158", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:57:11.670Z", + "updated_at": "2019-04-09T13:57:11.670Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158931064, + "type": null, + "body": "Thanks, @smcgivern! LGTM 👍", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:56:58.560Z", + "updated_at": "2019-04-09T13:56:58.560Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158930861, + "type": null, + "body": "approved this merge request", + "attachment": null, + "author": { + "id": 283999, + "name": "Douglas Barbosa Alexandre", + "username": "dbalexandre", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/283999/avatar.png", + "web_url": "https://gitlab.com/dbalexandre" + }, + "created_at": "2019-04-09T13:56:37.218Z", + "updated_at": "2019-04-09T13:56:37.218Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704850, + "type": null, + "body": "assigned to @dbalexandre", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:04.248Z", + "updated_at": "2019-04-09T06:46:04.248Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704847, + "type": null, + "body": "The reviewers and maintainers changing on each push was a bit confusing. Thanks for fixing!\n\n\n@dbalexandre Over to you :smile:.", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:04.013Z", + "updated_at": "2019-04-09T06:46:04.013Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704845, + "type": null, + "body": "approved this merge request", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:46:03.790Z", + "updated_at": "2019-04-09T06:46:03.790Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158704643, + "type": null, + "body": "resolved all discussions", + "attachment": null, + "author": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "created_at": "2019-04-09T06:45:15.102Z", + "updated_at": "2019-04-09T06:45:15.102Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158395805, + "type": null, + "body": "assigned to @reprazent", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:22:26.589Z", + "updated_at": "2019-04-08T11:22:26.589Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158395800, + "type": "DiscussionNote", + "body": "This MR:\n\n1. https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/192544935\n2. https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/192559554\n\nEE MR (https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/10640):\n\n1. https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/192548951\n2. https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/192559927\n\nAll picked the same two reviewers :tada: \n\nWhich means @reprazent again. Sorry about that!", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:22:26.118Z", + "updated_at": "2019-04-08T11:22:26.118Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": true, + "resolved": true, + "resolved_by": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "noteable_iid": 27117 + }, + { + "id": 158392374, + "type": "DiscussionNote", + "body": "\n## Reviewer roulette\n\nChanges that require review have been detected! A merge request is normally\nreviewed by both a reviewer and a maintainer in its primary category (e.g.\n~frontend or ~backend), and by a maintainer in all other categories.\n\n\n\nTo spread load more evenly across eligible reviewers, Danger has randomly picked\na candidate for each review slot. Feel free to override this selection if you\nthink someone else would be better-suited, or the chosen person is unavailable.\n\nOnce you've decided who will review this merge request, mention them as you\nnormally would! Danger does not (yet?) automatically notify them for you.\n\n| Category | Reviewer | Maintainer |\n| -------- | -------- | ---------- |\n| ~backend | [Bob Van Landuyt](https://gitlab.com/reprazent) (`@reprazent`) | [Douglas Barbosa Alexandre](https://gitlab.com/dbalexandre) (`@dbalexandre`) |\n\n

\n Generated by :no_entry_sign: Danger\n

\n", + "attachment": null, + "author": { + "id": 1786152, + "name": "🤖 GitLab Bot 🤖", + "username": "gitlab-bot", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/1786152/avatar.png", + "web_url": "https://gitlab.com/gitlab-bot" + }, + "created_at": "2019-04-08T11:12:37.422Z", + "updated_at": "2019-04-08T11:22:26.212Z", + "system": false, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": true, + "resolved": true, + "resolved_by": { + "id": 171554, + "name": "Bob Van Landuyt", + "username": "reprazent", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/171554/avatar.png", + "web_url": "https://gitlab.com/reprazent" + }, + "noteable_iid": 27117 + }, + { + "id": 158388876, + "type": null, + "body": "mentioned in merge request gitlab-ee!10640", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T11:02:38.416Z", + "updated_at": "2019-04-08T11:02:38.416Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158387911, + "type": null, + "body": "mentioned in issue #57766", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:59:49.849Z", + "updated_at": "2019-04-08T10:59:49.849Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + }, + { + "id": 158387842, + "type": null, + "body": "changed milestone to %\"11.11\"", + "attachment": null, + "author": { + "id": 443319, + "name": "Sean McGivern", + "username": "smcgivern", + "state": "active", + "avatar_url": "https://gl-canary.freetls.fastly.net/uploads/-/system/user/avatar/443319/avatar.png", + "web_url": "https://gitlab.com/smcgivern" + }, + "created_at": "2019-04-08T10:59:38.656Z", + "updated_at": "2019-04-08T10:59:38.656Z", + "system": true, + "noteable_id": 27253868, + "noteable_type": "MergeRequest", + "resolvable": false, + "noteable_iid": 27117 + } + ], + "rawHeaders": [ + "Server", + "nginx", + "Date", + "Mon, 20 May 2019 11:19:03 GMT", + "Content-Type", + "application/json", + "Content-Length", + "9682", + "Connection", + "close", + "Cache-Control", + "max-age=0, private, must-revalidate", + "Etag", + "W/\"edab8aad8eea37dd376785c34c7c250f\"", + "Link", + "; rel=\"first\", ; rel=\"last\"", + "Vary", + "Origin", + "X-Content-Type-Options", + "nosniff", + "X-Frame-Options", + "SAMEORIGIN", + "X-Next-Page", + "", + "X-Page", + "1", + "X-Per-Page", + "20", + "X-Prev-Page", + "", + "X-Request-Id", + "yd9SuXYdPCa", + "X-Runtime", + "1.305536", + "X-Total", + "15", + "X-Total-Pages", + "1", + "Strict-Transport-Security", + "max-age=31536000", + "Referrer-Policy", + "strict-origin-when-cross-origin", + "RateLimit-Limit", + "600", + "RateLimit-Observed", + "1", + "RateLimit-Remaining", + "599", + "RateLimit-Reset", + "1558351203", + "RateLimit-ResetTime", + "Mon, 20 May 2019 11:20:03 GMT" + ] + } +] diff --git a/source/platforms/gitlab/_tests/fixtures/getUser.json b/source/platforms/gitlab/_tests/fixtures/getUser.json new file mode 100644 index 000000000..aaa3001b6 --- /dev/null +++ b/source/platforms/gitlab/_tests/fixtures/getUser.json @@ -0,0 +1,91 @@ +[ + { + "scope": "https://gitlab.com:443", + "method": "GET", + "path": "/api/v4/user", + "body": "", + "status": 200, + "rawHeaders": [ + "Server", + "nginx", + "Date", + "Mon, 20 May 2019 11:16:46 GMT", + "Content-Type", + "application/json", + "Content-Length", + "978", + "Connection", + "close", + "Cache-Control", + "max-age=0, private, must-revalidate", + "Etag", + "W/\"f11855b852551e25670833a47ebd0244\"", + "Vary", + "Origin", + "X-Content-Type-Options", + "nosniff", + "X-Frame-Options", + "SAMEORIGIN", + "X-Request-Id", + "4QnQ6n8cn7a", + "X-Runtime", + "0.029428", + "Strict-Transport-Security", + "max-age=31536000", + "Referrer-Policy", + "strict-origin-when-cross-origin", + "RateLimit-Limit", + "600", + "RateLimit-Observed", + "2", + "RateLimit-Remaining", + "598", + "RateLimit-Reset", + "1558351066", + "RateLimit-ResetTime", + "Mon, 20 May 2019 11:17:46 GMT" + ], + "response": { + "id": 92864, + "name": "First Last", + "username": "username", + "state": "active", + "avatar_url": "https://www.", + "web_url": "https://www.", + "created_at": "2015-02-04T09:26:00.089Z", + "bio": "", + "location": "", + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": "My Organization", + "last_sign_in_at": "2019-05-16T08:16:29.198Z", + "confirmed_at": "2015-02-04T09:25:59.877Z", + "last_activity_on": "2019-05-20", + "email": "username@example.com", + "theme_id": 1, + "color_scheme_id": 3, + "projects_limit": 100000, + "current_sign_in_at": "2019-05-17T08:30:42.586Z", + "identities": [ + { + "provider": "google_oauth2", + "extern_uid": "xxxx" + }, + { + "provider": "github", + "extern_uid": "xxxx" + } + ], + "can_create_group": true, + "can_create_project": true, + "two_factor_enabled": true, + "external": false, + "private_profile": false, + "shared_runners_minutes_limit": 2000, + "extra_shared_runners_minutes_limit": null + } + } +] diff --git a/source/platforms/platform.ts b/source/platforms/platform.ts index 0e44dab82..9093265c9 100644 --- a/source/platforms/platform.ts +++ b/source/platforms/platform.ts @@ -1,9 +1,11 @@ -import { Env, CISource } from "../ci_source/ci_source" -import { GitJSONDSL, GitDSL } from "../dsl/GitDSL" +import { CISource, Env } from "../ci_source/ci_source" +import { GitDSL, GitJSONDSL } from "../dsl/GitDSL" import { GitHub } from "./GitHub" import { GitHubAPI } from "./github/GitHubAPI" import { BitBucketServer } from "./BitBucketServer" import { BitBucketServerAPI, bitbucketServerRepoCredentialsFromEnv } from "./bitbucket_server/BitBucketServerAPI" +import GitLabAPI, { getGitLabAPICredentialsFromEnv } from "./gitlab/GitLabAPI" +import GitLab from "./GitLab" import { DangerResults } from "../dsl/DangerResults" import { ExecutorOptions } from "../runner/Executor" import { DangerRunner } from "../runner/runners/runner" @@ -36,7 +38,7 @@ export interface Platform extends PlatformCommunicator { readonly name: string getReviewInfo: () => Promise - /** Pulls in the platform specific metadata for code review runs */ + /** Pulls in the platform specific metadata for code review runs in JSON format */ getPlatformReviewDSLRepresentation: () => Promise /** Pulls in the platform specific metadata for event runs */ getPlatformReviewSimpleRepresentation?: () => Promise @@ -87,10 +89,9 @@ export interface PlatformCommunicator { * @param {CISource} source The existing source, to ensure they can run against each other * @returns {Platform} returns a platform if it can be supported */ -export function getPlatformForEnv(env: Env, source: CISource, requireAuth = true): Platform { +export function getPlatformForEnv(env: Env, source: CISource): Platform { // BitBucket Server - const bbsHost = env["DANGER_BITBUCKETSERVER_HOST"] - if (bbsHost) { + if (env["DANGER_BITBUCKETSERVER_HOST"] || env["DANGER_PR_PLATFORM"] === BitBucketServer.name) { const api = new BitBucketServerAPI( { pullRequestID: source.pullRequestID, @@ -98,8 +99,19 @@ export function getPlatformForEnv(env: Env, source: CISource, requireAuth = true }, bitbucketServerRepoCredentialsFromEnv(env) ) - const bbs = new BitBucketServer(api) - return bbs + return new BitBucketServer(api) + } + + // GitLab + if (env["DANGER_GITLAB_API_TOKEN"] || env["DANGER_PR_PLATFORM"] === GitLab.name) { + const api = new GitLabAPI( + { + pullRequestID: source.pullRequestID, + repoSlug: source.repoSlug, + }, + getGitLabAPICredentialsFromEnv(env) + ) + return new GitLab(api) } // They need to set the token up for GitHub actions to work @@ -113,15 +125,14 @@ export function getPlatformForEnv(env: Env, source: CISource, requireAuth = true // GitHub Platform const ghToken = env["DANGER_GITHUB_API_TOKEN"] || env["GITHUB_TOKEN"] - if (ghToken || !requireAuth) { + if (ghToken || env["DANGER_PR_PLATFORM"] === GitHub.name) { if (!ghToken) { console.log("You don't have a DANGER_GITHUB_API_TOKEN set up, this is optional, but TBH, you want to do this") console.log("Check out: http://danger.systems/js/guides/the_dangerfile.html#working-on-your-dangerfile") } const api = new GitHubAPI(source, ghToken) - const github = GitHub(api) - return github + return GitHub(api) } // Support automatically returning a fake platform if you pass a Fake CI @@ -129,7 +140,9 @@ export function getPlatformForEnv(env: Env, source: CISource, requireAuth = true return new FakePlatform() } - console.error("The DANGER_GITHUB_API_TOKEN/DANGER_BITBUCKETSERVER_HOST environmental variable is missing") + console.error( + "The DANGER_GITHUB_API_TOKEN/DANGER_BITBUCKETSERVER_HOST/DANGER_GITLAB_API_TOKEN environmental variable is missing" + ) console.error("Without an api token, danger will be unable to comment on a PR") throw new Error("Cannot use authenticated API requests.") } diff --git a/source/platforms/pullRequestParser.ts b/source/platforms/pullRequestParser.ts index 13a4c769e..65ce35642 100644 --- a/source/platforms/pullRequestParser.ts +++ b/source/platforms/pullRequestParser.ts @@ -1,9 +1,13 @@ import * as url from "url" import includes from "lodash.includes" +import { BitBucketServer } from "./BitBucketServer" +import { GitHub } from "./GitHub" +import GitLab from "./GitLab" export interface PullRequestParts { pullRequestNumber: string repo: string + platform: string } export function pullRequestParser(address: string): PullRequestParts | null { @@ -14,6 +18,7 @@ export function pullRequestParser(address: string): PullRequestParts | null { const parts = components.path.match(/(projects\/\w+\/repos\/[\w-_.]+)\/pull-requests\/(\d+)/) if (parts) { return { + platform: BitBucketServer.name, repo: parts[1], pullRequestNumber: parts[2], } @@ -22,10 +27,24 @@ export function pullRequestParser(address: string): PullRequestParts | null { // shape: http://github.com/proj/repo/pull/1 if (includes(components.path, "pull")) { return { + platform: GitHub.name, repo: components.path.split("/pull")[0].slice(1), pullRequestNumber: components.path.split("/pull/")[1], } } + + // shape: https://gitlab.com/GROUP[/SUBGROUP]/PROJ/merge_requests/123 + if (includes(components.path, "merge_requests")) { + const regex = /\/(.+)\/merge_requests\/(\d+)/ + const parts = components.path.match(regex) + if (parts) { + return { + platform: GitLab.name, + repo: parts[1], + pullRequestNumber: parts[2], + } + } + } } return null diff --git a/source/runner/Executor.ts b/source/runner/Executor.ts index 0047e93db..e76349410 100644 --- a/source/runner/Executor.ts +++ b/source/runner/Executor.ts @@ -251,7 +251,7 @@ export class Executor { await this.platform.deleteMainComment(dangerID) const previousComments = await this.platform.getInlineComments(dangerID) for (const comment of previousComments) { - if (comment) { + if (comment && comment.ownedByDanger) { await this.deleteInlineComment(comment) } } @@ -277,6 +277,7 @@ export class Executor { this.platform.deleteMainComment(dangerID) } else { const commitID = git.commits[git.commits.length - 1].sha + // TODO: GitLab template formatting (or reuse one of the others?) const comment = process.env["DANGER_BITBUCKETSERVER_HOST"] ? bitbucketServerTemplate(dangerID, commitID, mergedResults) : githubResultsTemplate(dangerID, commitID, mergedResults) diff --git a/source/runner/dslGenerator.ts b/source/runner/dslGenerator.ts index ce21308e4..1a3d06f3f 100644 --- a/source/runner/dslGenerator.ts +++ b/source/runner/dslGenerator.ts @@ -28,9 +28,12 @@ export const jsonDSLGenerator = async ( textOnly: program.textOnly, verbose: program.verbose, } + + const dslPlatformName = jsonDSLPlatformName(platform) + return { git, - [platform.name === "BitBucketServer" ? "bitbucket_server" : "github"]: platformDSL, + [dslPlatformName]: platformDSL, settings: { github: { accessToken: process.env["DANGER_GITHUB_API_TOKEN"] || process.env["GITHUB_TOKEN"] || "NO_TOKEN", @@ -41,3 +44,15 @@ export const jsonDSLGenerator = async ( }, } } + +const jsonDSLPlatformName = (platform: Platform): string => { + switch (platform.name) { + case "BitBucketServer": + return "bitbucket_server" + case "GitLab": + return "gitlab" + case "GitHub": + default: + return "github" + } +} diff --git a/source/runner/jsonToDSL.ts b/source/runner/jsonToDSL.ts index 4a733ec77..49abc5a55 100644 --- a/source/runner/jsonToDSL.ts +++ b/source/runner/jsonToDSL.ts @@ -14,19 +14,23 @@ import { import { CISource } from "../ci_source/ci_source" import { debug } from "../debug" +import { gitlabJSONToGitLabDSL } from "../platforms/GitLab" +import GitLabAPI, { getGitLabAPICredentialsFromEnv } from "../platforms/gitlab/GitLabAPI" +import { gitLabGitDSL } from "../platforms/gitlab/GitLabGit" const d = debug("jsonToDSL") /** - * Re-hydrates the JSON DSL that is passed from the host process into the full DAnger DSL + * Re-hydrates the JSON DSL that is passed from the host process into the full Danger DSL */ export const jsonToDSL = async (dsl: DangerDSLJSONType, source: CISource): Promise => { // In a GitHub Action you could be running on other event types d(`Creating ${source && source.useEventDSL ? "event" : "pr"} DSL from JSON`) const api = apiForDSL(dsl) - const platformExists = [dsl.github, dsl.bitbucket_server].some(p => !!p) + const platformExists = [dsl.github, dsl.bitbucket_server, dsl.gitlab].some(p => !!p) const github = dsl.github && githubJSONToGitHubDSL(dsl.github, api as OctoKit) const bitbucket_server = dsl.bitbucket_server + const gitlab = dsl.gitlab && gitlabJSONToGitLabDSL(dsl.gitlab, api as GitLabAPI) let git: GitDSL if (!platformExists) { @@ -34,6 +38,8 @@ export const jsonToDSL = async (dsl: DangerDSLJSONType, source: CISource): Promi git = await localPlatform.getPlatformGitRepresentation() } else if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { git = bitBucketServerGitDSL(bitbucket_server!, dsl.git, api as BitBucketServerAPI) + } else if (process.env["DANGER_GITLAB_API_TOKEN"]) { + git = gitLabGitDSL(gitlab!, dsl.git) } else { git = source && source.useEventDSL ? ({} as any) : githubJSONToGitDSL(github!, dsl.git) } @@ -45,6 +51,7 @@ export const jsonToDSL = async (dsl: DangerDSLJSONType, source: CISource): Promi // which just doesn't feel right. github: github!, bitbucket_server: bitbucket_server!, + gitlab: gitlab!, utils: { sentence, href, @@ -52,11 +59,17 @@ export const jsonToDSL = async (dsl: DangerDSLJSONType, source: CISource): Promi } } -const apiForDSL = (dsl: DangerDSLJSONType): OctoKit | BitBucketServerAPI => { +const apiForDSL = (dsl: DangerDSLJSONType): OctoKit | BitBucketServerAPI | GitLabAPI => { if (process.env["DANGER_BITBUCKETSERVER_HOST"]) { return new BitBucketServerAPI(dsl.bitbucket_server!.metadata, bitbucketServerRepoCredentialsFromEnv(process.env)) } + const gitlab = dsl.gitlab + if (gitlab != null && process.env["DANGER_GITLAB_API_TOKEN"] != null) { + // d({ gitlab }) + return new GitLabAPI(gitlab.metadata, getGitLabAPICredentialsFromEnv(process.env)) + } + const options: OctoKit.Options & { debug: boolean } = { debug: !!process.env.LOG_FETCH_REQUESTS, baseUrl: dsl.settings.github.baseURL, diff --git a/yarn.lock b/yarn.lock index f0ada99e5..99350d1c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -929,6 +929,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" integrity sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o= +"@types/nock@^10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@types/nock/-/nock-10.0.3.tgz#dab1d18ffbccfbf2db811dab9584304eeb6e1c4c" + integrity sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.1.2.tgz#8c5da14d70321e4c4ecd5db668e3f93cf6c7399f" @@ -937,9 +944,9 @@ "@types/node" "*" "@types/node@*": - version "8.0.46" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.46.tgz#6e1766b2d0ed06631d5b5f87bb8e72c8dbb6888e" - integrity sha512-rRkP4kb5JYIfAoRKaDbcdPZBcTNOgzSApyzhPN9e6rhViSJAWQGlSXIX5gc75iR02jikhpzy3usu31wMHllfFw== + version "12.0.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40" + integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA== "@types/node@^10.11.3": version "10.11.3" @@ -991,6 +998,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" @@ -1207,6 +1221,11 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" +array-uniq@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d" + integrity sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0= + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -1232,6 +1251,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -2185,6 +2209,18 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chai@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2237,6 +2273,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + chokidar@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" @@ -2410,6 +2451,13 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +combined-stream@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^2.11.0: version "2.12.2" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" @@ -2968,6 +3016,18 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +deep-equal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3422,6 +3482,11 @@ etag@~1.8.0: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + exec-sh@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38" @@ -3754,6 +3819,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" @@ -3890,6 +3964,11 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + get-own-enumerable-property-symbols@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" @@ -3988,6 +4067,20 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +gitlab@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gitlab/-/gitlab-6.0.0.tgz#7bd44bf57dcb6b1231c70a52ea9b83934e6c1539" + integrity sha512-/0KE/Un0Hg/CDTtiYfC1Z4lkuHhkSmC3JjkKy4VsCgTb51rTtOfk7NGspyM+wTIgEPd6cE061Pu5m4a5ao01Ow== + dependencies: + form-data "^2.3.3" + humps "^2.0.1" + ky "^0.11.0" + ky-universal "^0.2.1" + li "^1.3.0" + query-string "^6.5.0" + randomstring "^1.1.5" + universal-url "^2.0.0" + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -4259,6 +4352,11 @@ has@^1.0.1: dependencies: function-bind "^1.0.2" +hasurl@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hasurl/-/hasurl-1.0.0.tgz#e4c619097ae1e8fc906bee904ce47e94f5e1ea37" + integrity sha512-43ypUd3DbwyCT01UYpA99AEZxZ4aKtRxWGBHEIbjcOsUghd9YUON0C+JF6isNjaiwC/UF5neaUudy6JS9jZPZQ== + hawk@~6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" @@ -4346,6 +4444,11 @@ https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" +humps@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" + integrity sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao= + husky@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-1.0.1.tgz#749bc6b3a14bdc9cab73d8cc827b92fcd691fac6" @@ -5542,6 +5645,19 @@ kleur@^3.0.0: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.1.tgz#4f5b313f5fa315432a400f19a24db78d451ede62" integrity sha512-P3kRv+B+Ra070ng2VKQqW4qW7gd/v3iD8sy/zOdcYRsfiD+QBokQNOps/AfP6Hr48cBhIIBFWckB9aO+IZhrWg== +ky-universal@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.2.1.tgz#9b499cf7442b9aa6c7a10c30aaee85bba044e244" + integrity sha512-6G7P8WrEcqTvdT+8f6hU1EBWib9mdW/n8S0M2Y0OC6WwlTMwYjQ06EYpijPwIMQlfeV22z7GP3NTVPBFn2RoBg== + dependencies: + abort-controller "^3.0.0" + node-fetch "^2.3.0" + +ky@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.11.0.tgz#032e01e10979f9f16682f6acff7daf43cafa5507" + integrity sha512-8EHh1PfPKrERUf5XTSSUkuvfMygg6qyFmW7fqjjHxoGTySMQ5TfF006QenZmtcSSbJmd71wLYY04xxIS7vNpJg== + latest-version@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" @@ -5591,6 +5707,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +li@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" + integrity sha1-IsWbyu+qmo7zWc91l4TkvxBq6hs= + lint-staged@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-7.3.0.tgz#90ff33e5ca61ed3dbac35b6f6502dbefdc0db58d" @@ -5894,11 +6015,16 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lowercase-keys@1.0.0, lowercase-keys@^1.0.0: +lowercase-keys@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + lru-cache@^4.0.0, lru-cache@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" @@ -6135,6 +6261,11 @@ micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: snapdragon "^0.8.1" to-regex "^3.0.2" +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + "mime-db@>= 1.29.0 < 2", mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" @@ -6164,7 +6295,14 @@ mime-types@2.1.20: dependencies: mime-db "~1.36.0" -mime-types@^2.1.12, mime-types@~2.1.16, mime-types@~2.1.17: +mime-types@^2.1.12: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mime-types@~2.1.16, mime-types@~2.1.17: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo= @@ -6366,6 +6504,21 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nock@^10.0.6: + version "10.0.6" + resolved "https://registry.yarnpkg.com/nock/-/nock-10.0.6.tgz#e6d90ee7a68b8cfc2ab7f6127e7d99aa7d13d111" + integrity sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w== + dependencies: + chai "^4.1.2" + debug "^4.1.0" + deep-equal "^1.0.0" + json-stringify-safe "^5.0.1" + lodash "^4.17.5" + mkdirp "^0.5.0" + propagate "^1.0.0" + qs "^6.5.1" + semver "^5.5.0" + node-cleanup@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c" @@ -6987,6 +7140,11 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -7205,6 +7363,11 @@ prompts@^2.0.1: kleur "^3.0.0" sisteransi "^1.0.0" +propagate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709" + integrity sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk= + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -7238,6 +7401,11 @@ q@^1.4.1, q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@^6.5.1: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -7252,11 +7420,27 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +query-string@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.5.0.tgz#2e1a70125af01f6f04573692d02c09302a1d8bfc" + integrity sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +randomstring@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/randomstring/-/randomstring-1.1.5.tgz#6df0628f75cbd5932930d9fe3ab4e956a18518c3" + integrity sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM= + dependencies: + array-uniq "1.0.2" + range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -8231,6 +8415,11 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" integrity sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA== +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -8312,6 +8501,11 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-argv@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736" @@ -8658,7 +8852,7 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.3: dependencies: punycode "^1.4.1" -tr46@^1.0.0: +tr46@^1.0.0, tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= @@ -8775,6 +8969,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -8933,6 +9132,14 @@ unique-temp-dir@1.0.0: os-tmpdir "^1.0.1" uid2 "0.0.3" +universal-url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universal-url/-/universal-url-2.0.0.tgz#35e7fc2c3374804905cee67ea289ed3a47669809" + integrity sha512-3DLtXdm/G1LQMCnPj+Aw7uDoleQttNHp2g5FnNQKR6cP6taNWS1b/Ehjjx4PVyvejKi3TJyu8iBraKM4q3JQPg== + dependencies: + hasurl "^1.0.0" + whatwg-url "^7.0.0" + universal-user-agent@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.1.tgz#18e591ca52b1cb804f6b9cbc4c336cf8191f80e1" @@ -9153,6 +9360,15 @@ whatwg-url@^6.3.0: tr46 "^1.0.0" webidl-conversions "^4.0.1" +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"