diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index 7190d1950f67d1b5fc995a932aac180766ca1022..66e64f0d682c84ac5a4399a6438c3a83a110ed99 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -28,6 +28,9 @@ module.exports = { commands: { executeCommand: jest.fn(), }, + workspace: { + getConfiguration: jest.fn().mockReturnValue({ instanceUrl: 'https://gitlab.com' }), + }, CommentMode: { Preview: 1 }, CommentThreadCollapsibleState: { Expanded: 1 }, Position: function Position(x, y) { diff --git a/src/git_service.test.ts b/src/git_service.test.ts index ee726d74b876cf683b710528d4e04d5ef4bc615c..c323d2235fb535b7e4708efb7185cd60c5a153f8 100644 --- a/src/git_service.test.ts +++ b/src/git_service.test.ts @@ -22,10 +22,8 @@ describe('git_service', () => { const getDefaultOptions = (): GitServiceOptions => ({ workspaceFolder, - instanceUrl: 'https://gitlab.com', remoteName: undefined, pipelineGitRemoteName: undefined, - tokenService: { getInstanceUrls: () => [] }, log: () => { // }, diff --git a/src/git_service.ts b/src/git_service.ts index 0e6a2c16602972980282204aa47939ea7e6fc383..2bea1da4ace3894c08b5fa4cb8a6bb7a5d47e2c6 100644 --- a/src/git_service.ts +++ b/src/git_service.ts @@ -1,53 +1,35 @@ import * as execa from 'execa'; -import * as url from 'url'; -import { GITLAB_COM_URL } from './constants'; import { parseGitRemote, GitRemote } from './git/git_remote_parser'; - -interface TokenService { - getInstanceUrls(): string[]; -} +import { getInstanceUrl } from './utils/get_instance_url'; export interface GitServiceOptions { workspaceFolder: string; - instanceUrl?: string; remoteName?: string; pipelineGitRemoteName?: string; - tokenService: TokenService; log: (line: string) => void; } export class GitService { workspaceFolder: string; - instanceUrl?: string; - remoteName?: string; pipelineGitRemoteName?: string; - tokenService: TokenService; - log: (line: string) => void; constructor(options: GitServiceOptions) { - this.instanceUrl = options.instanceUrl; this.remoteName = options.remoteName; this.pipelineGitRemoteName = options.pipelineGitRemoteName; this.workspaceFolder = options.workspaceFolder; - this.tokenService = options.tokenService; this.log = options.log; } private async fetch(cmd: string): Promise { const [git, ...args] = cmd.trim().split(' '); - let currentWorkspaceFolder = this.workspaceFolder; - - if (currentWorkspaceFolder == null) { - currentWorkspaceFolder = ''; - } try { const { stdout } = await execa(git, args, { - cwd: currentWorkspaceFolder, + cwd: this.workspaceFolder, preferLocal: false, }); return stdout; @@ -74,7 +56,7 @@ export class GitService { } if (remoteUrl) { - return parseGitRemote(await this.fetchCurrentInstanceUrl(), remoteUrl); + return parseGitRemote(await getInstanceUrl(this.workspaceFolder), remoteUrl); } return null; @@ -121,74 +103,4 @@ export class GitService { return branchName; } - - private async fetchGitRemoteUrls(): Promise { - const fetchGitRemotesVerbose = async (): Promise => { - const output = await this.fetch('git remote -v'); - - return (output || '').split('\n'); - }; - - const parseRemoteFromVerboseLine = (line: string) => { - // git remote -v output looks like - // origin[TAB]git@gitlab.com:gitlab-org/gitlab-vscode-extension.git[WHITESPACE](fetch) - // the interesting part is surrounded by a tab symbol and a whitespace - - return line.split(/\t| /)[1]; - }; - - const remotes = await fetchGitRemotesVerbose(); - const remoteUrls = remotes.map(remote => parseRemoteFromVerboseLine(remote)).filter(Boolean); - - // git remote -v returns a (fetch) and a (push) line for each remote, - // so we need to remove duplicates - return [...new Set(remoteUrls)]; - } - - private async intersectionOfInstanceAndTokenUrls() { - const uriHostname = (uri: string) => url.parse(uri).host; - - const instanceUrls = this.tokenService.getInstanceUrls(); - const gitRemotes = await this.fetchGitRemoteUrls(); - const gitRemoteHosts = gitRemotes.map(uriHostname); - - return instanceUrls.filter(host => gitRemoteHosts.includes(uriHostname(host))); - } - - private async heuristicInstanceUrl() { - // if the intersection of git remotes and configured PATs exists and is exactly - // one hostname, use it - const intersection = await this.intersectionOfInstanceAndTokenUrls(); - if (intersection.length === 1) { - const heuristicUrl = intersection[0]; - this.log( - `Found ${heuristicUrl} in the PAT list and git remotes, using it as the instanceUrl`, - ); - return heuristicUrl; - } - - if (intersection.length > 1) { - this.log( - `Found more than one intersection of git remotes and configured PATs, ${intersection}`, - ); - } - - return null; - } - - async fetchCurrentInstanceUrl(): Promise { - // if the workspace setting exists, use it - if (this.instanceUrl) { - return this.instanceUrl; - } - - // try to determine the instance URL heuristically - const heuristicUrl = await this.heuristicInstanceUrl(); - if (heuristicUrl) { - return heuristicUrl; - } - - // default to Gitlab cloud - return GITLAB_COM_URL; - } } diff --git a/src/gitlab_service.ts b/src/gitlab_service.ts index 802a886cc34c42381d02c1af000a6679412fb0e7..820250ec679a36b4c28339b9be0f6414d8a07547 100644 --- a/src/gitlab_service.ts +++ b/src/gitlab_service.ts @@ -13,6 +13,7 @@ import { CustomQueryType } from './gitlab/custom_query_type'; import { CustomQuery } from './gitlab/custom_query'; import { getAvatarUrl } from './utils/get_avatar_url'; import { getHttpAgentOptions } from './utils/get_http_agent_options'; +import { getInstanceUrl as getInstanceUrlUtil } from './utils/get_instance_url'; interface GitLabProject { id: number; @@ -46,12 +47,7 @@ const normalizeAvatarUrl = (instanceUrl: string) => (issuable: RestIssuable): Re const projectCache: Record = {}; let versionCache: string | null = null; -const getInstanceUrl = async () => - await createGitService( - // fetching of instanceUrl is the only GitService method that doesn't need workspaceFolder - // TODO: remove this default value once we implement https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/260 - (await getCurrentWorkspaceFolder()) || '', - ).fetchCurrentInstanceUrl(); +const getInstanceUrl = async () => await getInstanceUrlUtil(await getCurrentWorkspaceFolder()); async function fetch(path: string, method = 'GET', data?: Record) { const instanceUrl = await getInstanceUrl(); diff --git a/src/search_input.js b/src/search_input.js index 8beb5744187eeac0afcbc877c5086e6e452e48b2..3d397702f84084c7a1a4be720a87ecf436fa8b1a 100644 --- a/src/search_input.js +++ b/src/search_input.js @@ -2,7 +2,7 @@ const vscode = require('vscode'); const gitLabService = require('./gitlab_service'); const openers = require('./openers'); const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service'); -const { createGitService } = require('./service_factory'); +const { getInstanceUrl } = require('./utils/get_instance_url'); const parseQuery = (query, noteableType) => { const params = {}; @@ -121,7 +121,7 @@ async function showProjectAdvancedSearchInput() { 'Project Advanced Search. (Check extension page for Advanced Search)', ); const queryString = await encodeURIComponent(query); - const instanceUrl = await createGitService(workspaceFolder).fetchCurrentInstanceUrl(); + const instanceUrl = await getInstanceUrl(workspaceFolder); // Select issues tab by default for Advanced Search await openers.openUrl( diff --git a/src/service_factory.ts b/src/service_factory.ts index dac904f6770a2e4aafa32d99868f466c21fc414e..b4754cdaeff4137b643aa0706e2c6a0090d4433a 100644 --- a/src/service_factory.ts +++ b/src/service_factory.ts @@ -1,26 +1,21 @@ import * as vscode from 'vscode'; import { GitService } from './git_service'; -import { tokenService } from './services/token_service'; import { log } from './log'; import { GitLabNewService } from './gitlab/gitlab_new_service'; +import { getInstanceUrl } from './utils/get_instance_url'; export function createGitService(workspaceFolder: string): GitService { - const { instanceUrl, remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration( - 'gitlab', - ); + const { remoteName, pipelineGitRemoteName } = vscode.workspace.getConfiguration('gitlab'); // the getConfiguration() returns null for missing attributes, we need to convert them to // undefined so that we can use optional properties and default function parameters return new GitService({ workspaceFolder, - instanceUrl: instanceUrl || undefined, remoteName: remoteName || undefined, pipelineGitRemoteName: pipelineGitRemoteName || undefined, - tokenService, log, }); } export async function createGitLabNewService(workspaceFolder: string): Promise { - const gitService = createGitService(workspaceFolder); - return new GitLabNewService(await gitService.fetchCurrentInstanceUrl()); + return new GitLabNewService(await getInstanceUrl(workspaceFolder)); } diff --git a/src/services/workspace_service.ts b/src/services/workspace_service.ts index 5563fbf3168bcb411a914dbff2e7d5487617968e..a1b5eacf65ce307a2d1f09ffdbe2487c718f1913 100644 --- a/src/services/workspace_service.ts +++ b/src/services/workspace_service.ts @@ -9,7 +9,7 @@ async function getWorkspaceFolderForOpenEditor(): Promise { return workspaceFolder?.uri.fsPath; } -export async function getCurrentWorkspaceFolder(): Promise { +export async function getCurrentWorkspaceFolder(): Promise { const editorFolder = await getWorkspaceFolderForOpenEditor(); if (editorFolder) { @@ -21,7 +21,7 @@ export async function getCurrentWorkspaceFolder(): Promise { return workspaceFolders[0].uri.fsPath; } - return null; + return undefined; } export async function getCurrentWorkspaceFolderOrSelectOne(): Promise { diff --git a/src/utils/get_instance_url.ts b/src/utils/get_instance_url.ts new file mode 100644 index 0000000000000000000000000000000000000000..53aafcbf3dcc6de2d9781c2c3b80ea3de0ee145d --- /dev/null +++ b/src/utils/get_instance_url.ts @@ -0,0 +1,86 @@ +import * as vscode from 'vscode'; +import * as execa from 'execa'; +import * as url from 'url'; +import { GITLAB_COM_URL } from '../constants'; +import { tokenService } from '../services/token_service'; +import { log } from '../log'; + +async function fetch(cmd: string, workspaceFolder: string): Promise { + const [git, ...args] = cmd.trim().split(' '); + const { stdout } = await execa(git, args, { + cwd: workspaceFolder, + preferLocal: false, + }); + return stdout; +} + +async function fetchGitRemoteUrls(workspaceFolder: string): Promise { + const fetchGitRemotesVerbose = async (): Promise => { + const output = await fetch('git remote -v', workspaceFolder); + + return (output || '').split('\n'); + }; + + const parseRemoteFromVerboseLine = (line: string) => { + // git remote -v output looks like + // origin[TAB]git@gitlab.com:gitlab-org/gitlab-vscode-extension.git[WHITESPACE](fetch) + // the interesting part is surrounded by a tab symbol and a whitespace + + return line.split(/\t| /)[1]; + }; + + const remotes = await fetchGitRemotesVerbose(); + const remoteUrls = remotes.map(remote => parseRemoteFromVerboseLine(remote)).filter(Boolean); + + // git remote -v returns a (fetch) and a (push) line for each remote, + // so we need to remove duplicates + return [...new Set(remoteUrls)]; +} + +async function intersectionOfInstanceAndTokenUrls(workspaceFolder: string) { + const uriHostname = (uri: string) => url.parse(uri).host; + + const instanceUrls = tokenService.getInstanceUrls(); + const gitRemotes = await fetchGitRemoteUrls(workspaceFolder); + const gitRemoteHosts = gitRemotes.map(uriHostname); + + return instanceUrls.filter(host => gitRemoteHosts.includes(uriHostname(host))); +} + +async function heuristicInstanceUrl(workspaceFolder: string) { + // if the intersection of git remotes and configured PATs exists and is exactly + // one hostname, use it + const intersection = await intersectionOfInstanceAndTokenUrls(workspaceFolder); + if (intersection.length === 1) { + const heuristicUrl = intersection[0]; + log(`Found ${heuristicUrl} in the PAT list and git remotes, using it as the instanceUrl`); + return heuristicUrl; + } + + if (intersection.length > 1) { + log(`Found more than one intersection of git remotes and configured PATs, ${intersection}`); + } + + return null; +} + +export async function getInstanceUrl(workspaceFolder?: string): Promise { + const { instanceUrl } = vscode.workspace.getConfiguration('gitlab'); + // if the workspace setting exists, use it + if (instanceUrl) { + return instanceUrl; + } + + // legacy logic in GitLabService might not have the workspace folder available + // in that case we just skip the heuristic + if (workspaceFolder) { + // try to determine the instance URL heuristically + const heuristicUrl = await heuristicInstanceUrl(workspaceFolder); + if (heuristicUrl) { + return heuristicUrl; + } + } + + // default to Gitlab cloud + return GITLAB_COM_URL; +} diff --git a/test/integration/services/workspace_service.test.js b/test/integration/services/workspace_service.test.js index 0fc1434c769f78c149ef5babeec65e158b379233..825ebdcfff20a8d5737b1d6e2ea6148b8d9d2ba7 100644 --- a/test/integration/services/workspace_service.test.js +++ b/test/integration/services/workspace_service.test.js @@ -46,9 +46,9 @@ describe('workspace_service', () => { sandbox.restore(); }); - it('getCurrentWorkspaceFolder returns null', async () => { + it('getCurrentWorkspaceFolder returns undefined', async () => { const result = await workspaceService.getCurrentWorkspaceFolder(); - assert.strictEqual(result, null); + assert.strictEqual(result, undefined); }); it('getCurrentWorkspaceFolderOrSelectOne lets user select a workspace', async () => {