diff --git a/.vscode/launch.json b/.vscode/launch.json index ade7d621f..6431af9c9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "protocol": "inspector", "console": "internalConsole", "sourceMaps": true, - "outFiles": ["${workspaceRoot}/distribution"] + "outFiles": ["${workspaceRoot}/distribution"], + "runtimeExecutable": "/Users/joshua/.nvm/versions/node/v10.12.0/bin/node" } ] } diff --git a/source/commands/ci/runner.ts b/source/commands/ci/runner.ts index 77252b37f..b91d4f1ee 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/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 index 378bd198f..f863abdc2 100644 --- a/source/dsl/GitLabDSL.ts +++ b/source/dsl/GitLabDSL.ts @@ -1,13 +1,29 @@ +// TODO: extract out from BitBucket specifically, or create our own type +import { RepoMetaData } from "./BitBucketServerDSL" + +// danger.gitlab + +export interface GitLabDSL { + metadata: RepoMetaData + // issues: any[] + mr: GitLabMR + // commits: GitLabMRCommit[] + // comments: any[] +} + +// --- +// JSON responses from API + export interface GitLabUser { id: number name: string username: string - state: "active" + state: "active" // XXX: other states? avatar_url: string | null web_url: string } -export interface GitLabMRDSLBase { +export interface GitLabMRBase { /** */ id: number @@ -52,7 +68,7 @@ export interface GitLabMRDSLBase { project_id: number title: string description: string - state: "closed" + state: "closed" // XXX: other states? created_at: string updated_at: string due_date: string @@ -60,7 +76,7 @@ export interface GitLabMRDSLBase { web_url: string } merge_when_pipeline_succeeds: boolean - merge_status: "can_be_merged" + merge_status: "can_be_merged" // XXX: other statuses? merge_error: null | null sha: string merge_commit_sha: string | null @@ -79,7 +95,7 @@ export interface GitLabMRDSLBase { } } -export interface GitLabMRDSL extends GitLabMRDSLBase { +export interface GitLabMR extends GitLabMRBase { squash: boolean subscribed: boolean changes_count: string @@ -94,7 +110,7 @@ export interface GitLabMRDSL extends GitLabMRDSLBase { id: number sha: string ref: string - status: "success" + status: "success" // XXX: other statuses? web_url: string } diff_refs: { @@ -107,7 +123,7 @@ export interface GitLabMRDSL extends GitLabMRDSLBase { approvals_before_merge: null | null } -export interface GitLabMRChangeDSL { +export interface GitLabMRChange { old_path: string new_path: string a_mode: string @@ -118,11 +134,42 @@ export interface GitLabMRChangeDSL { deleted_file: boolean } -export interface GitLabMRChangesDSL extends GitLabMRDSLBase { - changes: GitLabMRChangeDSL[] +export interface GitLabMRChanges extends GitLabMRBase { + changes: GitLabMRChange[] +} + +export interface GitLabComment { + id: number + type: "DiffNote" | 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 GitLabInlineComment extends GitLabComment { + 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 GitLabMRCommitDSL { +export interface GitLabMRCommit { id: string short_id: string created_at: string diff --git a/source/platforms/GitLab.ts b/source/platforms/GitLab.ts index 2cd60c75c..d6033ad16 100644 --- a/source/platforms/GitLab.ts +++ b/source/platforms/GitLab.ts @@ -3,6 +3,7 @@ import { Platform, Comment } from "./platform" import { readFileSync } from "fs" import { GitDSL, GitJSONDSL } from "../dsl/GitDSL" import { GitCommit } from "../dsl/Commit" +import { GitLabDSL } from "../dsl/GitLabDSL" class GitLab implements Platform { public readonly name: string @@ -11,15 +12,28 @@ class GitLab implements Platform { this.name = "GitLab" } - async getReviewInfo(): Promise { - return this.api.getPullRequestInfo() + getReviewInfo = async (): Promise => { + return this.api.getMergeRequestInfo() } - async getPlatformReviewDSLRepresentation(): Promise { - return {} + // returns the `danger.gitlab` object + getPlatformReviewDSLRepresentation = async (): Promise => { + const mr = await this.getReviewInfo() + // const commits = await this.api.getMergeRequestCommits() + // const comments: any[] = [] //await this.api.getMergeRequestComments() + // const activities = {} //await this.api.getPullRequestActivities() + // const issues: any[] = [] //await this.api.getIssues() + + return { + metadata: this.api.repoMetadata, + // issues, + mr, + // commits, + // comments, + } } - async getPlatformGitRepresentation(): Promise { + getPlatformGitRepresentation = async (): Promise => { const changes = await this.api.getMergeRequestChanges() const commits = await this.api.getMergeRequestCommits() @@ -57,17 +71,25 @@ class GitLab implements Platform { modified_files, created_files, deleted_files, + commits: mappedCommits, // diffForFile: async () => ({ before: "", after: "", diff: "", added: "", removed: "" }), // structuredDiffForFile: async () => ({ chunks: [] }), // JSONDiffForFile: async () => ({} as any), // JSONPatchForFile: async () => ({} as any), - commits: mappedCommits, // linesOfCode: async () => 0, } } - async getInlineComments(_: string): Promise { - return [] + getInlineComments = async (_: string): Promise => { + const comments = (await this.api.getMergeRequestInlineComments()).map(comment => { + return { + id: `${comment.id}`, + body: comment.body, + ownedByDanger: comment.author.id === 1, + } + }) + + return comments } supportsCommenting() { @@ -78,31 +100,31 @@ class GitLab implements Platform { return true } - async updateOrCreateComment(_dangerID: string, _newComment: string): Promise { + updateOrCreateComment = async (_dangerID: string, _newComment: string): Promise => { return "https://gitlab.com/group/project/merge_requests/154#note_132143425" } - async createComment(_comment: string): Promise { + createComment = async (_comment: string): Promise => { return true } - async createInlineComment(_git: GitDSL, _comment: string, _path: string, _line: number): Promise { + createInlineComment = async (_git: GitDSL, _comment: string, _path: string, _line: number): Promise => { return true } - async updateInlineComment(_comment: string, _commentId: string): Promise { + updateInlineComment = async (_comment: string, _commentId: string): Promise => { return true } - async deleteInlineComment(_id: string): Promise { + deleteInlineComment = async (_id: string): Promise => { return true } - async deleteMainComment(): Promise { + deleteMainComment = async (): Promise => { return true } - async updateStatus(): Promise { + updateStatus = async (): Promise => { return true } diff --git a/source/platforms/_tests/_gitlab.test.ts b/source/platforms/_tests/_gitlab.test.ts new file mode 100644 index 000000000..e69de29bb 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 index b2fdc34e9..da0535c44 100644 --- a/source/platforms/gitlab/GitLabAPI.ts +++ b/source/platforms/gitlab/GitLabAPI.ts @@ -1,6 +1,13 @@ import { RepoMetaData } from "../../dsl/BitBucketServerDSL" import { api as fetch } from "../../api/fetch" -import { GitLabMRDSL, GitLabMRChangesDSL, GitLabMRChangeDSL, GitLabMRCommitDSL } from "../../dsl/GitLabDSL" +import { + GitLabMR, + GitLabMRChanges, + GitLabMRChange, + GitLabMRCommit, + GitLabInlineComment, + GitLabComment, +} from "../../dsl/GitLabDSL" import { Gitlab } from "gitlab" // const Gitlab = require("gitlab").default @@ -10,7 +17,7 @@ export type GitLabAPIToken = string class GitLabAPI { fetch: typeof fetch - private pr: GitLabMRDSL | undefined + // private mr: GitLabMR | undefined // https://github.com/jdalrymple/node-gitlab/issues/257 private api: any //typeof Gitlab @@ -45,14 +52,19 @@ class GitLabAPI { return `${this.projectURL}/merge_requests/${this.repoMetadata.pullRequestID}` } - getPullRequestInfo = async (): Promise => { - if (this.pr) { - return this.pr - } + getMergeRequestInfo = async (): Promise => { + // if (this.mr) { + // return this.mr + // } + + const mr = (await this.api.MergeRequests.show( + this.repoMetadata.repoSlug, + this.repoMetadata.pullRequestID + )) as GitLabMR - console.log("[+] getPullRequestInfo") + // this.mr = mr - return this.api.MergeRequests.show(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + return mr // const repo = this.repoMetadata.repoSlug // const prID = this.repoMetadata.pullRequestID @@ -67,17 +79,29 @@ class GitLabAPI { // } } - getMergeRequestChanges = async (): Promise => { - const pr: GitLabMRChangesDSL = await this.api.MergeRequests.changes( + getMergeRequestChanges = async (): Promise => { + const mr = (await this.api.MergeRequests.changes( this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID - ) + )) as GitLabMRChanges + + return mr.changes + } + + getMergeRequestCommits = async (): Promise => { + return await this.api.MergeRequests.commits(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + } - return pr.changes + getMergeRequestComments = async (): Promise => { + const api = this.api.MergeRequestNotes() + return (await api.all(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID)) as GitLabComment[] } - getMergeRequestCommits = async (): Promise => { - return this.api.MergeRequests.commits(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + getMergeRequestInlineComments = async (): Promise => { + const api = this.api.MergeRequestNotes() + return (await api + .all(this.repoMetadata.repoSlug, this.repoMetadata.pullRequestID) + .filter((comment: GitLabComment) => comment.type == "DiffNote")) as GitLabInlineComment[] } } diff --git a/source/platforms/gitlab/GitLabGit.ts b/source/platforms/gitlab/GitLabGit.ts new file mode 100644 index 000000000..84ad81864 --- /dev/null +++ b/source/platforms/gitlab/GitLabGit.ts @@ -0,0 +1,27 @@ +import { debug } from "../../debug" +import { GitLabDSL } from "../../dsl/GitLabDSL" +import { GitJSONDSL, GitDSL } from "../../dsl/GitDSL" +import { GitJSONToGitDSLConfig, gitJSONToGitDSL, 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, + + // TODO: implement me when the API methods are in + getFileContents: async (): Promise => { + return "" + }, + // TODO: implement me when the API methods are in + getFullDiff: async (): Promise => { + return "" + }, + // TODO: implement me when the API methods are in + getStructuredDiffForFile: async (): Promise => Promise.resolve([]), + } + + d("Setting up git DSL with: ", config) + return gitJSONToGitDSL(json, config) +} 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..60feb3bee 100644 --- a/source/runner/jsonToDSL.ts +++ b/source/runner/jsonToDSL.ts @@ -14,19 +14,22 @@ import { import { CISource } from "../ci_source/ci_source" import { debug } from "../debug" +import GitLabAPI 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 let git: GitDSL if (!platformExists) { @@ -34,6 +37,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_HOST"]) { + git = gitLabGitDSL(gitlab!, dsl.git /*, api as GitLabAPI*/) } else { git = source && source.useEventDSL ? ({} as any) : githubJSONToGitDSL(github!, dsl.git) } @@ -45,6 +50,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 +58,20 @@ 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 + console.log("??????") + d("???????????") + d({ dsl }) + if (gitlab != null && process.env["DANGER_GITLAB_API_TOKEN"] != null) { + // d({ gitlab }) + return new GitLabAPI(gitlab.metadata, process.env["DANGER_GITLAB_API_TOKEN"]!) + } + const options: OctoKit.Options & { debug: boolean } = { debug: !!process.env.LOG_FETCH_REQUESTS, baseUrl: dsl.settings.github.baseURL,