From 621c3968083a7626436bed5be83613c3a409d33f Mon Sep 17 00:00:00 2001 From: Michael Aigner Date: Thu, 8 Jul 2021 07:49:12 +0000 Subject: [PATCH] feat(gitclone): add wiki repo clone support for git clone command --- package.json | 9 +++ src/__mocks__/vscode.js | 2 + src/command_names.ts | 2 + src/commands/clone_wiki.test.ts | 75 +++++++++++++++++ src/commands/clone_wiki.ts | 81 +++++++++++++++++++ src/extension.js | 2 + .../gitlab_remote_source_provider.test.ts | 19 +++++ .../clone/gitlab_remote_source_provider.ts | 36 +++++++-- src/gitlab/gitlab_project.ts | 4 + src/gitlab/graphql/shared.ts | 2 + src/test_utils/entities.ts | 1 + src/utils/show_quickpick.ts | 16 ++++ 12 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 src/commands/clone_wiki.test.ts create mode 100644 src/commands/clone_wiki.ts create mode 100644 src/gitlab/clone/gitlab_remote_source_provider.test.ts create mode 100644 src/utils/show_quickpick.ts diff --git a/package.json b/package.json index 40eda56..8363f25 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,11 @@ { "command": "gl.checkoutMrBranch", "title": "Checkout MR branch" + }, + { + "command": "gl.cloneWiki", + "title": "Clone Wiki", + "category": "GitLab" } ], "menus": { @@ -280,6 +285,10 @@ { "command": "gl.refreshSidebar", "when": "gitlab:validState" + }, + { + "command": "gl.cloneWiki", + "when": "!gitlab:noToken" } ], "view/title": [ diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index 74da277..8b92893 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -24,6 +24,8 @@ module.exports = { showErrorMessage: jest.fn(), createStatusBarItem: jest.fn(), withProgress: jest.fn(), + showQuickPick: jest.fn(), + createQuickPick: jest.fn(), }, commands: { executeCommand: jest.fn(), diff --git a/src/command_names.ts b/src/command_names.ts index 7e95b23..e5c877f 100644 --- a/src/command_names.ts +++ b/src/command_names.ts @@ -32,6 +32,7 @@ export const USER_COMMANDS = { SUBMIT_COMMENT_EDIT: 'gl.submitCommentEdit', CREATE_COMMENT: 'gl.createComment', CHECKOUT_MR_BRANCH: 'gl.checkoutMrBranch', + CLONE_WIKI: 'gl.cloneWiki', }; /* @@ -46,4 +47,5 @@ export const VS_COMMANDS = { DIFF: 'vscode.diff', OPEN: 'vscode.open', GIT_SHOW_OUTPUT: 'git.showOutput', + GIT_CLONE: 'git.clone', }; diff --git a/src/commands/clone_wiki.test.ts b/src/commands/clone_wiki.test.ts new file mode 100644 index 0000000..707d533 --- /dev/null +++ b/src/commands/clone_wiki.test.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode'; +import { cloneWiki } from './clone_wiki'; +import { tokenService } from '../services/token_service'; +import { RemoteSource } from '../api/git'; +import { GitLabRemoteSourceProvider } from '../gitlab/clone/gitlab_remote_source_provider'; +import { showQuickPick } from '../utils/show_quickpick'; + +jest.mock('../services/token_service'); +jest.mock('../gitlab/clone/gitlab_remote_source_provider'); +jest.mock('../utils/show_quickpick'); + +describe('cloneWiki', () => { + const wikiRemoteSource = { + name: `$(repo) gitlab-org/gitlab-vscode-extension`, + description: 'description', + url: [ + 'git@gitlab.com:gitlab-org/gitlab-vscode-extension.wiki.git', + 'https://gitlab.com/gitlab-org/gitlab-vscode-extension.wiki.git', + ], + }; + let wikiRemoteSources: RemoteSource[]; + let instanceUrls: string[]; + + const alwaysPickFirstOption = () => { + (vscode.window.showQuickPick as jest.Mock).mockImplementation(([option]) => option); + (showQuickPick as jest.Mock).mockImplementation(picker => picker.items[0]); + }; + + beforeEach(() => { + tokenService.getInstanceUrls = () => instanceUrls; + (GitLabRemoteSourceProvider as jest.Mock).mockImplementation(() => ({ + getRemoteWikiSources: () => wikiRemoteSources, + })); + wikiRemoteSources = [wikiRemoteSource]; + (vscode.window.createQuickPick as jest.Mock).mockImplementation(() => { + const picker = { + onDidChangeValue: jest.fn(), + items: [], + }; + return picker; + }); + }); + + it('skips selection of instance if there is only one', async () => { + instanceUrls = ['https://gitlab.com']; + alwaysPickFirstOption(); + + await cloneWiki(); + + expect(vscode.window.showQuickPick).toHaveBeenCalledTimes(1); + expect(vscode.window.createQuickPick).toHaveBeenCalledTimes(1); + }); + + it('asks for instance if there are multiple', async () => { + instanceUrls = ['https://gitlab.com', 'https://example.com']; + alwaysPickFirstOption(); + + await cloneWiki(); + + expect(vscode.window.showQuickPick).toHaveBeenCalledTimes(2); + expect(vscode.window.createQuickPick).toHaveBeenCalledTimes(1); + }); + + it('calls git.clone command with selected URL', async () => { + instanceUrls = ['https://gitlab.com']; + alwaysPickFirstOption(); + + await cloneWiki(); + + expect(vscode.commands.executeCommand).toBeCalledWith( + 'git.clone', + 'git@gitlab.com:gitlab-org/gitlab-vscode-extension.wiki.git', + ); + }); +}); diff --git a/src/commands/clone_wiki.ts b/src/commands/clone_wiki.ts new file mode 100644 index 0000000..bf67161 --- /dev/null +++ b/src/commands/clone_wiki.ts @@ -0,0 +1,81 @@ +import * as vscode from 'vscode'; +import { tokenService } from '../services/token_service'; +import { GitLabRemoteSourceProvider } from '../gitlab/clone/gitlab_remote_source_provider'; +import { VS_COMMANDS } from '../command_names'; +import { showQuickPick } from '../utils/show_quickpick'; + +interface RemoteSourceItem { + label: string; + url: string[]; + description: string; +} + +async function pickRemoteProvider(): Promise { + const instanceUrls = tokenService.getInstanceUrls(); + const instanceItems = instanceUrls.map(u => ({ + label: `$(project) ${u}`, + instance: u, + })); + if (instanceItems.length === 0) { + throw new Error('no GitLab instance found'); + } + let selectedInstanceUrl; + if (instanceItems.length === 1) { + [selectedInstanceUrl] = instanceItems; + } else { + selectedInstanceUrl = await vscode.window.showQuickPick(instanceItems, { + ignoreFocusOut: true, + placeHolder: 'Select GitLab instance', + }); + } + if (!selectedInstanceUrl) { + return undefined; + } + return new GitLabRemoteSourceProvider(selectedInstanceUrl.instance); +} + +async function pickRemoteWikiSource( + provider: GitLabRemoteSourceProvider, +): Promise { + const wikiPick = vscode.window.createQuickPick(); + wikiPick.ignoreFocusOut = true; + wikiPick.placeholder = 'Select GitLab project'; + const getSourceItemsForQuery = async (query?: string) => { + const sources = await provider.getRemoteWikiSources(query); + return sources.map(s => ({ + label: s.name, + url: s.url as string[], + description: s.description || '', + })); + }; + wikiPick.onDidChangeValue(async value => { + wikiPick.items = await getSourceItemsForQuery(value); + }); + wikiPick.items = await getSourceItemsForQuery(); + + const selectedSource = await showQuickPick(wikiPick); + return selectedSource; +} + +export async function cloneWiki(): Promise { + const provider = await pickRemoteProvider(); + if (!provider) { + return; + } + + const selectedSource = await pickRemoteWikiSource(provider); + if (!selectedSource) { + return; + } + + const selectedUrl = await vscode.window.showQuickPick(selectedSource.url, { + ignoreFocusOut: true, + placeHolder: 'Select URL to clone from', + }); + + if (!selectedUrl) { + return; + } + + await vscode.commands.executeCommand(VS_COMMANDS.GIT_CLONE, selectedUrl); +} diff --git a/src/extension.js b/src/extension.js index 52aad6e..035fb87 100644 --- a/src/extension.js +++ b/src/extension.js @@ -31,6 +31,7 @@ const { hasCommentsDecorationProvider } = require('./review/has_comments_decorat const { changeTypeDecorationProvider } = require('./review/change_type_decoration_provider'); const { checkVersion } = require('./utils/check_version'); const { checkoutMrBranch } = require('./commands/checkout_mr_branch'); +const { cloneWiki } = require('./commands/clone_wiki'); vscode.gitLabWorkflow = { sidebarDataProviders: [], @@ -90,6 +91,7 @@ const registerCommands = (context, outputChannel) => { [USER_COMMANDS.SUBMIT_COMMENT_EDIT]: submitEdit, [USER_COMMANDS.CREATE_COMMENT]: createComment, [USER_COMMANDS.CHECKOUT_MR_BRANCH]: checkoutMrBranch, + [USER_COMMANDS.CLONE_WIKI]: cloneWiki, [PROGRAMMATIC_COMMANDS.NO_IMAGE_REVIEW]: () => vscode.window.showInformationMessage("GitLab MR review doesn't support images yet."), }; diff --git a/src/gitlab/clone/gitlab_remote_source_provider.test.ts b/src/gitlab/clone/gitlab_remote_source_provider.test.ts new file mode 100644 index 0000000..52e5ff6 --- /dev/null +++ b/src/gitlab/clone/gitlab_remote_source_provider.test.ts @@ -0,0 +1,19 @@ +import { convertUrlToWikiUrl } from './gitlab_remote_source_provider'; + +describe('convertUrlToWikiUrl', () => { + test('should convert urls to wiki urls', () => { + expect(convertUrlToWikiUrl('git@gitlab.com:username/myproject.git')).toBe( + 'git@gitlab.com:username/myproject.wiki.git', + ); + expect(convertUrlToWikiUrl('https://gitlab.com/username/myproject.git')).toBe( + 'https://gitlab.com/username/myproject.wiki.git', + ); + expect(convertUrlToWikiUrl('https://gitlab.com/user.git./myproject.git')).toBe( + 'https://gitlab.com/user.git./myproject.wiki.git', + ); + expect(convertUrlToWikiUrl('https://gitlab.com/user.git./myproject')).toBe( + 'https://gitlab.com/user.git./myproject', + ); + expect(convertUrlToWikiUrl('wrong')).toBe('wrong'); + }); +}); diff --git a/src/gitlab/clone/gitlab_remote_source_provider.ts b/src/gitlab/clone/gitlab_remote_source_provider.ts index e3b94cb..99a5b02 100644 --- a/src/gitlab/clone/gitlab_remote_source_provider.ts +++ b/src/gitlab/clone/gitlab_remote_source_provider.ts @@ -2,6 +2,15 @@ import { RemoteSource, RemoteSourceProvider } from '../../api/git'; import { GitLabNewService } from '../gitlab_new_service'; const SEARCH_LIMIT = 30; +const getProjectQueryAttributes = { + membership: true, + limit: SEARCH_LIMIT, + searchNamespaces: true, +}; + +export function convertUrlToWikiUrl(url: string): string { + return url.replace(/\.git$/, '.wiki.git'); +} export class GitLabRemoteSourceProvider implements RemoteSourceProvider { name: string; @@ -17,18 +26,35 @@ export class GitLabRemoteSourceProvider implements RemoteSourceProvider { this.gitlabService = new GitLabNewService(this.url); } - async getRemoteSources(query?: string): Promise { + async getRemoteSources(query?: string): Promise { const projects = await this.gitlabService.getProjects({ search: query, - membership: true, - limit: SEARCH_LIMIT, - searchNamespaces: true, + ...getProjectQueryAttributes, }); - return projects?.map(project => ({ + return projects.map(project => ({ name: `$(repo) ${project.fullPath}`, description: project.description, url: [project.sshUrlToRepo, project.httpUrlToRepo], })); } + + async getRemoteWikiSources(query?: string): Promise { + const projects = await this.gitlabService.getProjects({ + search: query, + ...getProjectQueryAttributes, + }); + + const wikiprojects = projects.filter(project => project.wikiEnabled); + return wikiprojects.map(project => { + return { + name: `$(repo) ${project.fullPath}`, + description: project.description, + url: [ + convertUrlToWikiUrl(project.sshUrlToRepo), + convertUrlToWikiUrl(project.httpUrlToRepo), + ], + }; + }); + } } diff --git a/src/gitlab/gitlab_project.ts b/src/gitlab/gitlab_project.ts index 248c446..9073ece 100644 --- a/src/gitlab/gitlab_project.ts +++ b/src/gitlab/gitlab_project.ts @@ -39,4 +39,8 @@ export class GitLabProject { get groupRestId(): number | undefined { return this.gqlProject.group && getRestIdFromGraphQLId(this.gqlProject.group.id); } + + get wikiEnabled(): boolean { + return this.gqlProject.wikiEnabled; + } } diff --git a/src/gitlab/graphql/shared.ts b/src/gitlab/graphql/shared.ts index 41723e1..a07f1c0 100644 --- a/src/gitlab/graphql/shared.ts +++ b/src/gitlab/graphql/shared.ts @@ -9,6 +9,7 @@ export const fragmentProjectDetails = gql` sshUrlToRepo fullPath webUrl + wikiEnabled group { id } @@ -88,6 +89,7 @@ export interface GqlProject { fullPath: string; webUrl: string; group?: GqlGroup; + wikiEnabled: boolean; } export interface GqlProjectResult { diff --git a/src/test_utils/entities.ts b/src/test_utils/entities.ts index ca9f76f..32561e2 100644 --- a/src/test_utils/entities.ts +++ b/src/test_utils/entities.ts @@ -91,6 +91,7 @@ export const gqlProject: GqlProject = { group: { id: 'gid://gitlab/Group/9970', }, + wikiEnabled: false, }; export const project = new GitLabProject(gqlProject); diff --git a/src/utils/show_quickpick.ts b/src/utils/show_quickpick.ts new file mode 100644 index 0000000..a46d4b1 --- /dev/null +++ b/src/utils/show_quickpick.ts @@ -0,0 +1,16 @@ +import { QuickPick, QuickPickItem } from 'vscode'; + +export async function showQuickPick( + quickpick: QuickPick, +): Promise { + const result = await new Promise(res => { + quickpick.onDidHide(() => res(undefined)); + quickpick.onDidAccept(() => { + res(quickpick.selectedItems[0]); + quickpick.hide(); + }); + quickpick.show(); + }); + + return result; +} -- GitLab