diff --git a/package.json b/package.json index 55e9d93..e3350e5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "coding-plugin", "displayName": "CODING Merge Requests & Releases", "description": "CODING plugin for VS Code.", - "version": "0.3.0", + "version": "0.3.1", "publisher": "coding-net", "license": "MIT", "engines": { @@ -60,9 +60,34 @@ "command": "codingPlugin.switchRepo", "title": "Switch repo", "category": "Coding plugin" + }, + { + "command": "codingPlugin.diff.createComment", + "title": "Post comment", + "enablement": "!commentIsEmpty" + }, + { + "command": "codingPlugin.diff.replyComment", + "title": "Reply", + "enablement": "!commentIsEmpty" + }, + { + "command": "extension.diff.deleteComment", + "title": "Delete comment", + "#enablement": "!commentIsEmpty" } ], "menus": { + "commandPalette": [ + { + "command": "codingPlugin.diff.createComment", + "when": "false" + }, + { + "command": "codingPlugin.diff.replyComment", + "when": "false" + } + ], "view/title": [ { "command": "codingPlugin.newMrDesc", @@ -74,6 +99,24 @@ "when": "view == mrTreeView", "group": "navigation" } + ], + "comments/commentThread/context": [ + { + "command": "codingPlugin.diff.createComment", + "group": "inline", + "when": "commentThreadIsEmpty" + }, + { + "command": "codingPlugin.diff.replyComment", + "group": "inline", + "when": "!commentThreadIsEmpty" + } + ], + "comments/comment/title": [ + { + "command": "extension.diff.deleteComment", + "when": "comment == editable" + } ] }, "viewsWelcome": [ @@ -128,7 +171,7 @@ "watch:webviews": "webpack --watch --mode development", "lint": "eslint . --ext .ts,.tsx", "clean": "rm -rf node_modules/ && yarn install --prod=true --force && rm -rf node_modules/@babel node_modules/@types node_modules/react*", - "package": "npx vsce package", + "pk": "npx vsce package", "release": "npx vsce publish" }, "babel": { @@ -148,12 +191,14 @@ "nanoid": "^3.1.20", "react": "^17.0.0", "react-dom": "^17.0.0", - "styled-components": "^5.2.1" + "styled-components": "^5.2.1", + "turndown": "^7.0.0" }, "devDependencies": { "@svgr/webpack": "^5.5.0", "@types/react": "^16.9.53", "@types/react-dom": "^16.9.8", + "@types/turndown": "^5.0.0", "@typescript-eslint/eslint-plugin": "^3.0.2", "@typescript-eslint/parser": "^3.0.2", "babel-plugin-styled-components": "^1.12.0", diff --git a/src/codingServer.ts b/src/codingServer.ts index e259c0b..17c87ce 100644 --- a/src/codingServer.ts +++ b/src/codingServer.ts @@ -5,7 +5,7 @@ import got from 'got'; import { AuthFailResult, AuthSuccessResult, - CodingResponse, + ICodingResponse, IRepoListResponse, IMRDiffResponse, IMRDetailResponse, @@ -18,14 +18,20 @@ import { IMRContentResp, ICreateCommentResp, IMRStatusResp, + IMRCommentResp, + IFileDiffParam, + IFileDiffResp, + ILineNoteResp, + ILineNoteForm, } from 'src/typings/respResult'; + import { PromiseAdapter, promiseFromEvent, parseQuery, parseCloneUrl } from 'src/common/utils'; import { GitService } from 'src/common/gitService'; import { IRepoInfo, ISessionData, TokenType } from 'src/typings/commonTypes'; import { keychain } from 'src/common/keychain'; import Logger from 'src/common/logger'; -const AUTH_SERVER = `https://x5p7m.csb.app`; +const AUTH_SERVER = `https://x5p7m.csb.app/`; const ClientId = `ff768664c96d04235b1cc4af1e3b37a8`; const ClientSecret = `d29ebb32cab8b5f0a643b5da7dcad8d1469312c7`; @@ -219,7 +225,7 @@ export class CodingServer { public async getUserInfo(team: string, token: string = this._session?.accessToken || ``) { try { - const result: CodingResponse = await got + const result: ICodingResponse = await got .get(`https://${team || `codingcorp`}.coding.net/api/current_user`, { searchParams: { access_token: token, @@ -266,10 +272,10 @@ export class CodingServer { }; } - public async getMRList(repo?: string, status?: string): Promise { + public async getMRList(repo?: string, status?: string): Promise { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .get(`${repoApiPrefix}/merges/query`, { searchParams: { status, @@ -390,10 +396,10 @@ export class CodingServer { } } - public async getMRComments(iid: string) { + public async getMRComments(iid: string | number) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: IMRCommentResp = await got .get(`${repoApiPrefix}/merge/${iid}/comments`, { searchParams: { access_token: this._session?.accessToken, @@ -413,7 +419,7 @@ export class CodingServer { public async closeMR(iid: string) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .post(`${repoApiPrefix}/merge/${iid}/refuse`, { searchParams: { access_token: this._session?.accessToken, @@ -433,7 +439,7 @@ export class CodingServer { public async approveMR(iid: string) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .post(`${repoApiPrefix}/merge/${iid}/good`, { searchParams: { access_token: this._session?.accessToken, @@ -453,7 +459,7 @@ export class CodingServer { public async disapproveMR(iid: string) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .delete(`${repoApiPrefix}/merge/${iid}/good`, { searchParams: { access_token: this._session?.accessToken, @@ -473,7 +479,7 @@ export class CodingServer { public async mergeMR(iid: string) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .post(`${repoApiPrefix}/merge/${iid}/merge`, { searchParams: { access_token: this._session?.accessToken, @@ -496,7 +502,7 @@ export class CodingServer { public async updateMRTitle(iid: string, title: string) { try { const { repoApiPrefix } = await this.getApiPrefix(); - const result: CodingResponse = await got + const result: ICodingResponse = await got .put(`${repoApiPrefix}/merge/${iid}/update-title`, { searchParams: { access_token: this._session?.accessToken, @@ -626,7 +632,7 @@ export class CodingServer { public async addMRReviewers(iid: string, ids: number[]): Promise { const { repoApiPrefix } = await this.getApiPrefix(); - const tasks: Promise[] = ids.map((id) => { + const tasks: Promise[] = ids.map((id) => { return got .post(`${repoApiPrefix}/merge/${iid}/reviewers`, { searchParams: { @@ -636,7 +642,7 @@ export class CodingServer { }) .json(); }); - const result: PromiseSettledResult[] = await Promise.allSettled(tasks); + const result: PromiseSettledResult[] = await Promise.allSettled(tasks); const fulfilled = ids.reduce((res, cur, idx) => { if (result[idx].status === `fulfilled`) { res = res.concat(cur); @@ -649,7 +655,7 @@ export class CodingServer { public async removeMRReviewers(iid: string, ids: number[]): Promise { const { repoApiPrefix } = await this.getApiPrefix(); - const tasks: Promise[] = ids.map((id) => { + const tasks: Promise[] = ids.map((id) => { return got .delete(`${repoApiPrefix}/merge/${iid}/reviewers`, { searchParams: { @@ -659,7 +665,7 @@ export class CodingServer { }) .json(); }); - const result: PromiseSettledResult[] = await Promise.allSettled(tasks); + const result: PromiseSettledResult[] = await Promise.allSettled(tasks); const fulfilled = ids.reduce((res, cur, idx) => { if (result[idx].status === `fulfilled`) { res = res.concat(cur); @@ -715,6 +721,52 @@ export class CodingServer { } } + public async fetchFileDiffs(param: IFileDiffParam) { + try { + const { repoApiPrefix } = await this.getApiPrefix(); + const resp: IFileDiffResp = await got + .get(`${repoApiPrefix}/compare_with_path`, { + searchParams: { + access_token: this._session?.accessToken, + base: param.base, + compare: param.compare, + path: encodeURIComponent(param.path), + mergeRequestId: param.mergeRequestId, + }, + }) + .json(); + + if (resp.code) { + return Promise.reject(resp); + } + + return resp; + } catch (e) { + return Promise.reject(e); + } + } + + public async postLineNote(data: ILineNoteForm) { + try { + const { repoApiPrefix } = await this.getApiPrefix(); + const resp: ILineNoteResp = await got.post(`${repoApiPrefix}/line_notes`, { + resolveBodyOnly: true, + responseType: `json`, + searchParams: { + access_token: this._session?.accessToken, + }, + form: data, + }); + + if (resp.code) { + return Promise.reject(resp); + } + return resp; + } catch (e) { + return Promise.reject(e); + } + } + get loggedIn() { return this._loggedIn; } diff --git a/src/common/contants.ts b/src/common/contants.ts new file mode 100644 index 0000000..de39f53 --- /dev/null +++ b/src/common/contants.ts @@ -0,0 +1,3 @@ +export const MRUriScheme = `coding-mr`; + +export const EmptyUserAvatar = `https://coding-net-production-static-ci.codehub.cn/7167f369-59ff-4196-bb76-a9959cf2b906.png`; diff --git a/src/common/utils.ts b/src/common/utils.ts index a273934..5cc04ce 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -66,3 +66,15 @@ export function getNonce() { } return text; } + +const HunkRegExp = /@@.+@@/g; +export const isHunkLine = (hunk: string) => HunkRegExp.test(hunk); + +export const getDiffLineNumber = (hunk: string) => { + const matchedHunks = hunk.match(/[-+]\d+(,\d+)?/g) || []; + return matchedHunks.map((i) => { + const [start = 0, sum = 0] = i.match(/\d+/g)?.map((j) => +j) ?? []; + const end = start + sum > 0 ? start + sum - 1 : 0; + return [start, end]; + }); +}; diff --git a/src/extension.ts b/src/extension.ts index 9f58929..a3b0a9f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,12 +1,22 @@ import 'module-alias/register'; import * as vscode from 'vscode'; +import * as TurndownService from 'turndown'; import { uriHandler, CodingServer } from 'src/codingServer'; import { Panel } from 'src/panel'; import { IFileNode, MRTreeDataProvider } from 'src/tree/mrTree'; import { ReleaseTreeDataProvider } from 'src/tree/releaseTree'; -import { IRepoInfo, IMRWebViewDetail, ISessionData } from 'src/typings/commonTypes'; +import { + IRepoInfo, + IMRWebViewDetail, + ISessionData, + ICachedCommentThreads, + ICachedCommentController, +} from 'src/typings/commonTypes'; import { GitService } from 'src/common/gitService'; +import { MRUriScheme } from 'src/common/contants'; +import { IDiffComment, IMRData, IDiffFile } from 'src/typings/respResult'; +import { replyNote, ReviewComment, makeCommentRangeProvider } from 'src/reviewCommentController'; export async function activate(context: vscode.ExtensionContext) { await GitService.init(); @@ -42,6 +52,12 @@ export async function activate(context: vscode.ExtensionContext) { showCollapseAll: true, }); + const tdService = new TurndownService(); + const diffFileData: { [key: string]: IDiffFile } = {}; + const cachedCommentThreads: ICachedCommentThreads = {}; + const cachedCommentControllers: ICachedCommentController = {}; + let selectedMrFile = ``; + context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); context.subscriptions.push( vscode.commands.registerCommand('codingPlugin.showMROverview', async (mr: IMRWebViewDetail) => { @@ -184,21 +200,127 @@ export async function activate(context: vscode.ExtensionContext) { }), ); context.subscriptions.push( - vscode.commands.registerCommand(`codingPlugin.showDiff`, async (file: IFileNode) => { - const headUri = vscode.Uri.parse(file.path, false).with({ - // path: `${file.path}.txt`, - scheme: `mr`, - query: `commit=${file.newSha}&path=${file.path}`, - }); - const parentUri = headUri.with({ query: `commit=${file.oldSha}&path=${file.path}` }); - await vscode.commands.executeCommand( - `vscode.diff`, - parentUri, - headUri, - `${file.name} (Merge Request)`, - { preserveFocus: true }, - ); - }), + vscode.commands.registerCommand( + `codingPlugin.showDiff`, + async (file: IFileNode, mr: IMRData) => { + const curMrFile = `${mr.iid}/${file.path}`; + if (selectedMrFile === curMrFile) { + return; + } + selectedMrFile = curMrFile; + + const headUri = vscode.Uri.parse(file.path, false).with({ + scheme: MRUriScheme, + query: `leftSha=${file.oldSha}&rightSha=${file.newSha}&path=${file.path}&right=true&mr=${mr.iid}&id=${mr.id}`, + }); + const parentUri = headUri.with({ + query: `leftSha=${file.oldSha}&rightSha=${file.newSha}&path=${file.path}&right=false&mr=${mr.iid}&id=${mr.id}`, + }); + await vscode.commands.executeCommand( + `vscode.diff`, + parentUri, + headUri, + `${file.name} (Merge Request #${mr.iid})`, + { preserveFocus: true }, + ); + + const cacheId = `${mr.iid}/${file.path}`; + cachedCommentThreads[cacheId]?.forEach((c) => { + c?.dispose(); + }); + cachedCommentThreads[cacheId] = []; + + let commentController = cachedCommentControllers[cacheId]; + if (!commentController) { + commentController = vscode.comments.createCommentController( + `mr-${mr.iid}`, + `mr-${mr.iid}-comment-controller`, + ); + commentController.commentingRangeProvider = { + provideCommentingRanges: makeCommentRangeProvider(codingSrv, diffFileData), + }; + cachedCommentControllers[cacheId] = commentController; + } + + try { + const commentResp = await codingSrv.getMRComments(mr.iid); + + const validComments = (commentResp.data as IDiffComment[][]) + .filter((i) => { + const first = i[0]; + return !first.outdated && first.path === file.path; + }, []) + .reduce((ret, i) => { + const ident = `${i[0].change_type === 1 ? 'right' : 'left'}-${i[0].position}-${ + i[0].line + }`; + ret[ident] = (ret[ident] ?? []).concat(i); + return ret; + }, {} as { [key: string]: IDiffComment[] }); + + Object.values(validComments).forEach((i) => { + const root = i[0]; + const isRight = root.change_type === 1; + + const rootLine = root.diffFile.diffLines[root.diffFile.diffLines.length - 1]; + const lineNum = isRight ? rootLine.rightNo - 1 : rootLine.leftNo - 1; + const range = new vscode.Range(lineNum, 0, lineNum, 0); + + const commentList: vscode.Comment[] = i + .sort((a, b) => a.created_at - b.created_at) + .map((c) => { + const body = new vscode.MarkdownString(tdService.turndown(c.content)); + body.isTrusted = true; + const comment = new ReviewComment( + body, + vscode.CommentMode.Preview, + { + name: `${c.author.name}(${c.author.global_key})`, + iconPath: vscode.Uri.parse(c.author.avatar, false), + }, + undefined, + 'canDelete', + c.id, + ); + + return comment; + }); + + const commentThread = commentController.createCommentThread( + isRight ? headUri : parentUri, + range, + [], + ); + commentThread.comments = commentList; + commentThread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded; + + cachedCommentThreads[cacheId] = (cachedCommentThreads[mr.iid] ?? []).concat( + commentThread, + ); + }); + } finally { + } + }, + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + `codingPlugin.diff.createComment`, + async (reply: vscode.CommentReply) => { + const cachedThreadId = await replyNote(reply, context, codingSrv, diffFileData); + cachedCommentThreads[cachedThreadId].push(reply.thread); + }, + ), + ); + context.subscriptions.push( + vscode.commands.registerCommand( + `codingPlugin.diff.replyComment`, + async (reply: vscode.CommentReply) => { + const cachedThreadId = await replyNote(reply, context, codingSrv, diffFileData); + cachedCommentThreads[cachedThreadId].push(reply.thread); + }, + ), ); if (vscode.window.registerWebviewPanelSerializer) { diff --git a/src/reviewCommentController.ts b/src/reviewCommentController.ts new file mode 100644 index 0000000..042d946 --- /dev/null +++ b/src/reviewCommentController.ts @@ -0,0 +1,129 @@ +import * as vscode from 'vscode'; + +import { ISessionData, IDiffFileData } from 'src/typings/commonTypes'; +import { EmptyUserAvatar, MRUriScheme } from 'src/common/contants'; +import { CodingServer } from 'src/codingServer'; +import { IFileDiffParam } from 'src/typings/respResult'; +import { getDiffLineNumber, isHunkLine } from 'src/common/utils'; + +let commentIdx = 0; +export class ReviewComment implements vscode.Comment { + id: number; + label: string | undefined; + constructor( + public body: string | vscode.MarkdownString, + public mode: vscode.CommentMode, + public author: vscode.CommentAuthorInformation, + public parent?: vscode.CommentThread, + public contextValue?: string, + public commentId?: number, + ) { + this.id = commentId ?? ++commentIdx; + } +} + +export async function replyNote( + reply: vscode.CommentReply, + context: vscode.ExtensionContext, + codingSrv: CodingServer, + diffFileData: IDiffFileData, +) { + const params = new URLSearchParams(decodeURIComponent(reply.thread.uri.query)); + const isRight = params.get('right') === `true`; + const ident = `${params.get(`mr`)}/${params.get(`path`)}`; + const diffFile = diffFileData[ident]; + + const noteable_id = params.get('id') ?? ``; // mr index id + const commitId = isRight ? params.get('rightSha') : params.get('leftSha'); + const content = reply.text; + const noteable_type = `MergeRequestBean`; + const change_type = isRight ? 1 : 2; + const line = reply.thread.range.start.line + 1; + const path = encodeURIComponent(params.get(`path`) || ``); + const targetPos = diffFile.diffLines.find((i) => { + return i[isRight ? `rightNo` : `leftNo`] === line; + }); + const position = targetPos?.index ?? 0; + + try { + const resp = await codingSrv.postLineNote({ + noteable_id, + commitId: commitId ?? ``, + content, + noteable_type, + change_type, + line, + path, + position, + anchor: `diff-${diffFile.pathMD5}`, + }); + + const curUser = context.workspaceState.get(`session`); + const commentAuthor: vscode.CommentAuthorInformation = curUser?.user + ? { + name: `${curUser.user.name} (${curUser.user.global_key})`, + iconPath: vscode.Uri.parse(curUser.user.avatar, false), + } + : { + name: `vscode user`, + iconPath: vscode.Uri.parse(EmptyUserAvatar, false), + }; + const thread = reply.thread; + thread.contextValue = `editable`; + const newComment = new ReviewComment( + reply.text, + vscode.CommentMode.Preview, + commentAuthor, + thread, + thread.comments.length ? 'canDelete' : undefined, + resp.data.id, + ); + + thread.comments = [...thread.comments, newComment]; + } catch (e) {} + + return `${params.get('mr')}/${path}`; +} + +export const makeCommentRangeProvider = ( + codingSrv: CodingServer, + diffFileData: IDiffFileData, +) => async (document: vscode.TextDocument, token: vscode.CancellationToken) => { + if (document.uri.scheme !== MRUriScheme) { + return []; + } + + try { + const params = new URLSearchParams(decodeURIComponent(document.uri.query)); + const mrId = params.get('id') || ``; + let param: IFileDiffParam = { + path: params.get('path') ?? ``, + base: params.get('leftSha') ?? ``, + compare: params.get('rightSha') ?? ``, + mergeRequestId: mrId ?? ``, + }; + const fileIdent = `${params.get(`mr`)}/${params.get(`path`)}`; + + const { data } = await codingSrv.fetchFileDiffs(param); + diffFileData[fileIdent] = data; + const { diffLines } = data; + + const ret = diffLines.reduce((result, i) => { + const isHunk = isHunkLine(i.text); + if (!isHunk) { + return result; + } + + const [left, right] = getDiffLineNumber(i.text); + const [start, end] = params.get('right') === `true` ? right : left; + if (start > 0) { + result.push(new vscode.Range(start - 1, 0, end, 0)); + } + return result; + }, [] as vscode.Range[]); + return ret; + } catch (e) { + console.error('fetch diff lines failed.'); + return []; + } +}; diff --git a/src/tree/inMemMRContentProvider.ts b/src/tree/inMemMRContentProvider.ts index 154cf0d..f5a7f50 100644 --- a/src/tree/inMemMRContentProvider.ts +++ b/src/tree/inMemMRContentProvider.ts @@ -30,7 +30,7 @@ export class InMemMRContentProvider implements vscode.TextDocumentContentProvide token: vscode.CancellationToken, ): Promise { const params = new URLSearchParams(decodeURIComponent(uri.query)); - const commit = params.get(`commit`); + const commit = params.get(`right`) === `true` ? params.get(`rightSha`) : params.get('leftSha'); const path = params.get(`path`); return await this._service.getRemoteFileContent(`${commit}/${path}`); } diff --git a/src/tree/mrTree.ts b/src/tree/mrTree.ts index 9e42dee..de71d5e 100644 --- a/src/tree/mrTree.ts +++ b/src/tree/mrTree.ts @@ -6,6 +6,7 @@ import { CodingServer } from 'src/codingServer'; import { IRepoInfo, ISessionData, GitChangeType } from 'src/typings/commonTypes'; import { IMRDiffStat, IMRData, IMRPathItem } from 'src/typings/respResult'; import { getInMemMRContentProvider } from './inMemMRContentProvider'; +import { MRUriScheme } from 'src/common/contants'; enum MRType { Open = `open`, @@ -69,7 +70,7 @@ export class MRTreeDataProvider implements vscode.TreeDataProvider { (f.children || [])?.length > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None, + this.value, ), ), ]; @@ -297,6 +299,7 @@ export class FileNode extends ListItem { public readonly label: string, public readonly value: IFileNode, public readonly collapsibleState: TreeItemCollapsibleState, + public readonly mrData: IMRData, ) { super( label, @@ -306,7 +309,7 @@ export class FileNode extends ListItem { ? { command: `codingPlugin.showDiff`, title: ``, - arguments: [value], + arguments: [value, mrData], } : undefined, FileNode.getFileIcon(value.changeType, collapsibleState), @@ -333,6 +336,7 @@ export class FileNode extends ListItem { (f.children || [])?.length > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None, + this.mrData, ), ); } diff --git a/src/typings/commonTypes.ts b/src/typings/commonTypes.ts index 7ba3245..0fc9534 100644 --- a/src/typings/commonTypes.ts +++ b/src/typings/commonTypes.ts @@ -1,4 +1,5 @@ -import { IMRDetail, IMRStatusItem, IUserItem } from './respResult'; +import * as vscode from 'vscode'; +import { IDiffFile, IMRDetail, IMRStatusItem, IUserItem } from './respResult'; export interface IRepoInfo { team: string; @@ -41,3 +42,15 @@ export interface IMRWebViewDetail { }; user: IUserItem; } + +export interface IDiffFileData { + [key: string]: IDiffFile; +} + +export interface ICachedCommentThreads { + [key: string]: vscode.CommentThread[]; +} + +export interface ICachedCommentController { + [key: string]: vscode.CommentController; +} diff --git a/src/typings/respResult.ts b/src/typings/respResult.ts index abe2816..fca33aa 100644 --- a/src/typings/respResult.ts +++ b/src/typings/respResult.ts @@ -36,13 +36,13 @@ export interface IMRData { updated_at: number; } -export interface CodingResponse { +export interface ICodingResponse { code: number; data?: any; msg?: string; } -export interface IListResponse extends CodingResponse { +export interface IListResponse extends ICodingResponse { data: { list: T[]; page: number; @@ -61,7 +61,7 @@ export interface IRepoItem { gitSshUrl: string; } -export interface IRepoListResponse extends CodingResponse { +export interface IRepoListResponse extends ICodingResponse { data: IRepoItem[]; } @@ -86,7 +86,7 @@ export interface IMRDiffStat { paths: IMRPathItem[]; } -export interface IMRDiffResponse extends CodingResponse { +export interface IMRDiffResponse extends ICodingResponse { data: { isLarge: boolean; diffStat: IMRDiffStat; @@ -141,7 +141,7 @@ export interface IMRDetail { can_merge: boolean; } -export interface IMRDetailResponse extends CodingResponse { +export interface IMRDetailResponse extends ICodingResponse { data: IMRDetail; } @@ -155,7 +155,7 @@ export interface IActivity { }; } -export interface IMRActivitiesResponse extends CodingResponse { +export interface IMRActivitiesResponse extends ICodingResponse { data: IActivity[]; } @@ -170,7 +170,7 @@ export interface IMRReviewers { reviewers: IReviewer[]; } -export interface IMRReviewersResponse extends CodingResponse { +export interface IMRReviewersResponse extends ICodingResponse { data: IMRReviewers; } @@ -197,7 +197,7 @@ export interface ICreateMRBody { watchers?: string; } -export interface ICreateMRResp extends CodingResponse { +export interface ICreateMRResp extends ICodingResponse { can_edit: boolean; can_edit_src_branch: boolean; merge_request: IMRDetail; @@ -214,7 +214,7 @@ export interface IBranchItem { status_check: boolean; } -export interface IBranchListResp extends CodingResponse { +export interface IBranchListResp extends ICodingResponse { data: IBranchItem[]; } @@ -237,11 +237,11 @@ export interface IMRContent { body_plan: string; } -export interface IMRContentResp extends CodingResponse { +export interface IMRContentResp extends ICodingResponse { data: IMRContent; } -export interface ICreateCommentResp extends CodingResponse { +export interface ICreateCommentResp extends ICodingResponse { data: IComment; } @@ -263,6 +263,109 @@ export interface IMRStatus { statuses: IMRStatusItem[]; } -export interface IMRStatusResp extends CodingResponse { +export interface IMRStatusResp extends ICodingResponse { data: IMRStatus; } + +export interface IDiffLine { + index: number; + leftNo: number; + rightNo: number; + prefix: string; + text: string; +} + +export interface IDiffFile { + linkRef: string; + linkUrl: string; + path: string; + changeType: string; + fileType: string; + type: string; + pathMD5: string; + changeMode: string; + oldMode: string; + newMode: string; + language: string; + insertions: number; + deletions: number; + isChangeInfoValid: boolean; + diffLines: IDiffLine[]; +} + +export interface IChildComment { + id: number; + noteable_type: string; + noteable_id: number; + content: string; + outdated: boolean; + author: IUserItem; + parentId: number; + created_at: number; + hasResourceReference: boolean; +} + +export interface IDiffComment { + id: number; + commitId: string; + noteable_type: string; + noteable_id: number; + line: number; + change_type: number; + position: number; + path: string; + anchor: string; + content: string; + outdated: boolean; + parentId: number; + created_at: number; + hasResourceReference: boolean; + author: IUserItem; + diffFile: IDiffFile; +} + +export interface IActivityComment { + id: number; + noteable_type: string; + noteable_id: number; + content: string; + outdated: boolean; + parentId: number; + created_at: number; + hasResourceReference: boolean; + author: IUserItem; + childComments: IChildComment[]; +} + +export type IMRComment = IDiffComment | IActivityComment; + +export interface IMRCommentResp extends ICodingResponse { + data: IMRComment[][]; +} + +export interface IFileDiffParam { + path: string; + base: string; + compare: string; + mergeRequestId: string; +} + +export interface IFileDiffResp extends ICodingResponse { + data: IDiffFile; +} + +export interface ILineNoteResp extends ICodingResponse { + data: IDiffComment; +} + +export interface ILineNoteForm { + commitId: string; + line: number; + change_type: number; + position: number; + path: string; + content: string; + noteable_type: string; + noteable_id: number | string; + anchor?: string; +} diff --git a/tsconfig.json b/tsconfig.json index 6bc79fe..1780b6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,5 +24,5 @@ "src/*": ["src/*"] } }, - "include": ["webviews", "types"] + "include": ["webviews", "src/typings"] } diff --git a/yarn.lock b/yarn.lock index 9d9b592..9bf0a76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1445,6 +1445,11 @@ dependencies: "@types/node" "*" +"@types/turndown@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.0.tgz#2b763b36f783e4e237cea62cdc8f8592b72b9285" + integrity sha512-Y7KZn6SfSv1vzjByJGijrM9w99HoUbLiAgYwe+sGH6RYi5diIGLYOcr4Z1YqR2biFs9K5PnxKv/Fbkmh57pvzg== + "@typescript-eslint/eslint-plugin@^3.0.2": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f" @@ -3138,6 +3143,11 @@ domelementtype@^2.0.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== +domino@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" + integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== + domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -7295,6 +7305,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +turndown@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.0.0.tgz#19b2a6a2d1d700387a1e07665414e4af4fec5225" + integrity sha512-G1FfxfR0mUNMeGjszLYl3kxtopC4O9DRRiMlMDDVHvU1jaBkGFg4qxIyjIk2aiKLHyDyZvZyu4qBO2guuYBy3Q== + dependencies: + domino "^2.1.6" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"