提交 8406e3a8 编写于 作者: T Tomas Vik

refactor: expose GitService and GitLab service in WrappedRepository

上级 0a7b9524
import * as vscode from 'vscode';
import { gitExtensionWrapper } from '../git/git_extension_wrapper';
import { GqlBlob, GqlSnippet } from '../gitlab/graphql/get_snippets';
import { createGitService, createGitLabNewService } from '../service_factory';
const pickSnippet = async (snippets: GqlSnippet[]) => {
const quickPickItems = snippets.map(s => ({
......@@ -31,10 +30,10 @@ export const insertSnippet = async (): Promise<void> => {
if (!repository) {
return;
}
const gitService = createGitService(repository.rootFsPath);
const gitLabService = await createGitLabNewService(repository.rootFsPath);
const remote = await gitService.fetchGitRemote();
const snippets = await gitLabService.getSnippets(`${remote.namespace}/${remote.project}`);
const remote = await repository.gitService.fetchGitRemote();
const snippets = await repository.gitLabService.getSnippets(
`${remote.namespace}/${remote.project}`,
);
if (snippets.length === 0) {
vscode.window.showInformationMessage('There are no project snippets.');
return;
......@@ -49,7 +48,7 @@ export const insertSnippet = async (): Promise<void> => {
if (!blob) {
return;
}
const snippet = await gitLabService.getSnippetContent(result.original, blob);
const snippet = await repository.gitLabService.getSnippetContent(result.original, blob);
const editor = vscode.window.activeTextEditor;
await editor.edit(editBuilder => {
editBuilder.insert(editor.selection.start, snippet);
......
......@@ -33,8 +33,8 @@ describe('GitExtensionWrapper', () => {
});
describe('repositories', () => {
const fakeRepository = createFakeRepository('/repository/root/path/');
const fakeRepository2 = createFakeRepository('/repository/root/path2/');
const fakeRepository = createFakeRepository({ rootUriPath: '/repository/root/path/' });
const fakeRepository2 = createFakeRepository({ rootUriPath: '/repository/root/path2/' });
it('returns no repositories when the extension is disabled', () => {
fakeExtension.gitApi.repositories = [fakeRepository];
......@@ -104,5 +104,14 @@ describe('GitExtensionWrapper', () => {
fakeRepository2.rootUri.fsPath,
]);
});
it('returns repository wrapped repository for a repositoryRootPath', () => {
fakeExtension.gitApi.repositories = [fakeRepository, fakeRepository2];
wrapper.init();
const repository = wrapper.getRepository('/repository/root/path/');
expect(repository?.rootFsPath).toBe('/repository/root/path/');
});
});
});
......@@ -48,6 +48,12 @@ export class GitExtensionWrapper implements vscode.Disposable {
return this.wrappedRepositories;
}
getRepository(repositoryRoot: string): WrappedRepository | undefined {
const rawRepository = this.gitApi?.getRepository(vscode.Uri.file(repositoryRoot));
if (!rawRepository) return undefined;
return this.repositories.find(r => r.hasSameRootAs(rawRepository));
}
private register() {
assert(this.gitExtension);
try {
......
import * as vscode from 'vscode';
import { WrappedRepository } from './wrapped_repository';
import { getExtensionConfiguration } from '../utils/get_extension_configuration';
import { tokenService } from '../services/token_service';
import { createFakeRepository } from '../test_utils/fake_git_extension';
import { Repository } from '../api/git';
import { GITLAB_COM_URL } from '../constants';
jest.mock('../utils/get_extension_configuration');
describe('WrappedRepository', () => {
let repository: Repository;
let wrappedRepository: WrappedRepository;
beforeEach(() => {
repository = createFakeRepository();
wrappedRepository = new WrappedRepository(repository);
});
describe('instanceUrl', () => {
let tokens = {};
const fakeContext = {
globalState: {
get: () => tokens,
},
};
beforeEach(() => {
tokens = {};
tokenService.init((fakeContext as any) as vscode.ExtensionContext);
});
it('should return configured instanceUrl', async () => {
(getExtensionConfiguration as jest.Mock).mockReturnValue({
instanceUrl: 'https://test.com',
});
expect(wrappedRepository.instanceUrl).toBe('https://test.com');
});
it('returns default instanceUrl when there is no configuration', async () => {
(getExtensionConfiguration as jest.Mock).mockReturnValue({});
expect(wrappedRepository.instanceUrl).toBe(GITLAB_COM_URL);
});
describe('heuristic', () => {
it('returns instanceUrl when there is exactly one match between remotes and token URLs', async () => {
repository = createFakeRepository({
remotes: [
'https://git@gitlab.com/gitlab-org/gitlab-vscode-extension.git',
'https://git@test-instance.com/g/extension.git',
],
});
tokens = {
'https://test-instance.com': 'abc',
};
wrappedRepository = new WrappedRepository(repository);
expect(wrappedRepository.instanceUrl).toBe('https://test-instance.com');
});
it('returns default instanceUrl when there is multiple matches between remotes and token URLs', async () => {
repository = createFakeRepository({
remotes: [
'https://git@gitlab.com/gitlab-org/gitlab-vscode-extension.git',
'https://git@test-instance.com/g/extension.git',
],
});
tokens = {
'https://test-instance.com': 'abc',
'https://gitlab.com': 'def',
};
wrappedRepository = new WrappedRepository(repository);
expect(wrappedRepository.instanceUrl).toBe(GITLAB_COM_URL);
});
});
});
});
import * as url from 'url';
import { basename } from 'path';
import { Repository } from '../api/git';
import { GITLAB_COM_URL } from '../constants';
import { tokenService } from '../services/token_service';
import { log } from '../log';
import { parseGitRemote } from './git_remote_parser';
import { getExtensionConfiguration } from '../utils/get_extension_configuration';
import { GitLabNewService } from '../gitlab/gitlab_new_service';
import { GitService } from '../git_service';
function intersectionOfInstanceAndTokenUrls(gitRemoteHosts: string[]) {
const instanceUrls = tokenService.getInstanceUrls();
return instanceUrls.filter(instanceUrl =>
gitRemoteHosts.includes(url.parse(instanceUrl).host || ''),
);
}
function heuristicInstanceUrl(gitRemoteHosts: string[]) {
// if the intersection of git remotes and configured PATs exists and is exactly
// one hostname, use it
const intersection = intersectionOfInstanceAndTokenUrls(gitRemoteHosts);
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 function getInstanceUrlFromRemotes(gitRemoteUrls: string[]): string {
const { instanceUrl } = getExtensionConfiguration();
// if the workspace setting exists, use it
if (instanceUrl) {
return instanceUrl;
}
// try to determine the instance URL heuristically
const gitRemoteHosts = gitRemoteUrls
.map((uri: string) => parseGitRemote(uri)?.host)
.filter((h): h is string => Boolean(h));
const heuristicUrl = heuristicInstanceUrl(gitRemoteHosts);
if (heuristicUrl) {
return heuristicUrl;
}
// default to Gitlab cloud
return GITLAB_COM_URL;
}
export class WrappedRepository {
private readonly rawRepository: Repository;
......@@ -8,6 +62,25 @@ export class WrappedRepository {
this.rawRepository = rawRepository;
}
get instanceUrl(): string {
const remoteUrls = this.rawRepository.state.remotes
.map(r => r.fetchUrl)
.filter((r): r is string => Boolean(r));
return getInstanceUrlFromRemotes(remoteUrls);
}
get gitLabService(): GitLabNewService {
return new GitLabNewService(this.instanceUrl);
}
get gitService(): GitService {
const { remoteName } = getExtensionConfiguration();
return new GitService({
repositoryRoot: this.rootFsPath,
preferredRemoteName: remoteName,
});
}
get name(): string {
return basename(this.rawRepository.rootUri.fsPath);
}
......
......@@ -5,6 +5,9 @@ import * as path from 'path';
import simpleGit, { SimpleGit } from 'simple-git';
import { GitService, GitServiceOptions } from './git_service';
import { gitExtensionWrapper } from './git/git_extension_wrapper';
import { getInstanceUrl } from './utils/get_instance_url';
jest.mock('./utils/get_instance_url');
const isMac = () => Boolean(process.platform.match(/darwin/));
......@@ -37,9 +40,7 @@ describe('git_service', () => {
});
beforeEach(() => {
(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
instanceUrl: 'https://gitlab.com',
});
(getInstanceUrl as jest.Mock).mockReturnValue('https://gitlab.com');
jest.spyOn(gitExtensionWrapper, 'gitBinaryPath', 'get').mockReturnValue('git');
});
......
......@@ -40,7 +40,7 @@ const projectCache: Record<string, GitLabProject> = {};
let versionCache: string | null = null;
async function fetch(
repositoryRoot: string | undefined,
repositoryRoot: string,
path: string,
method = 'GET',
data?: Record<string, unknown>,
......@@ -161,7 +161,7 @@ export async function fetchCurrentUser(repositoryRoot: string): Promise<RestUser
}
}
async function fetchFirstUserByUsername(repositoryRoot: string | undefined, userName: string) {
async function fetchFirstUserByUsername(repositoryRoot: string, userName: string) {
try {
const { response: users } = await fetch(repositoryRoot, `/users?username=${userName}`);
return users[0];
......@@ -360,7 +360,7 @@ export async function fetchIssuables(params: CustomQuery, repositoryRoot: string
const { response } = await fetch(repositoryRoot, `${path}?${search.toString()}`);
issuable = response;
return issuable.map(normalizeAvatarUrl(await getInstanceUrl()));
return issuable.map(normalizeAvatarUrl(await getInstanceUrl(repositoryRoot)));
}
export async function fetchLastJobsForCurrentBranch(
......
......@@ -2,7 +2,6 @@ import * as path from 'path';
import * as vscode from 'vscode';
import * as assert from 'assert';
import * as gitLabService from './gitlab_service';
import { createGitService } from './service_factory';
import { handleError } from './log';
import { VS_COMMANDS } from './command_names';
import { gitExtensionWrapper } from './git/git_extension_wrapper';
......@@ -68,10 +67,9 @@ async function getActiveFile() {
return undefined;
}
const gitService = createGitService(repository.rootFsPath);
const branchName = await gitService.fetchTrackingBranchName();
const branchName = await repository.gitService.fetchTrackingBranchName();
const filePath = path.relative(
await gitService.getRepositoryRootFolder(),
await repository.gitService.getRepositoryRootFolder(),
editor.document.uri.fsPath,
);
const fileUrl = `${currentProject!.webUrl}/blob/${branchName}/${filePath}`;
......@@ -120,7 +118,7 @@ export async function openCreateNewMr(): Promise<void> {
const repository = await gitExtensionWrapper.getActiveRepositoryOrSelectOne();
if (!repository) return;
const project = await gitLabService.fetchCurrentProject(repository.rootFsPath);
const branchName = await createGitService(repository.rootFsPath).fetchTrackingBranchName();
const branchName = await repository.gitService.fetchTrackingBranchName();
openUrl(`${project!.webUrl}/merge_requests/new?merge_request%5Bsource_branch%5D=${branchName}`);
}
......@@ -142,7 +140,7 @@ export async function compareCurrentBranch(): Promise<void> {
if (!repository) return;
const project = await gitLabService.fetchCurrentProject(repository.rootFsPath);
const lastCommitId = await createGitService(repository.rootFsPath).fetchLastCommitId();
const lastCommitId = await repository.gitService.fetchLastCommitId();
if (project && lastCommitId) {
openUrl(`${project.webUrl}/compare/master...${lastCommitId}`);
......
......@@ -7,8 +7,21 @@ const removeFromArray = (array: any[], element: any): any[] => {
return array.filter(el => el !== element);
};
export const createFakeRepository = (rootUriPath: string): Repository =>
(({ rootUri: vscode.Uri.file(rootUriPath) } as unknown) as Repository);
export const fakeStateOptions = {
rootUriPath: '/path/to/repo',
remotes: ['git@a.com:gitlab/extension.git', 'git@b.com:gitlab/extension.git'],
};
export const createFakeRepository = (
options: Partial<typeof fakeStateOptions> = {},
): Repository => {
const { rootUriPath, remotes } = { ...fakeStateOptions, ...options };
return ({
rootUri: vscode.Uri.file(rootUriPath),
state: {
remotes: remotes.map(r => ({ fetchUrl: r })),
},
} as unknown) as Repository;
};
/**
* This is a simple test double for the native Git extension API
......@@ -40,6 +53,10 @@ class FakeGitApi {
};
}
getRepository(uri: vscode.Uri) {
return this.repositories.find(r => r.rootUri.toString() === uri.toString());
}
registerRemoteSourceProvider(provider: any) {
this.remoteSourceProviders.push(provider);
return {
......
......@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { CONFIG_NAMESPACE } from '../constants';
interface ExtensionConfiguration {
instanceUrl?: string;
remoteName?: string;
pipelineGitRemoteName?: string;
featureFlags?: string[];
......@@ -13,6 +14,7 @@ const turnNullToUndefined = <T>(val: T | null | undefined): T | undefined => val
export function getExtensionConfiguration(): ExtensionConfiguration {
const workspaceConfig = vscode.workspace.getConfiguration(CONFIG_NAMESPACE);
return {
instanceUrl: turnNullToUndefined(workspaceConfig.instanceUrl),
remoteName: turnNullToUndefined(workspaceConfig.remoteName),
pipelineGitRemoteName: turnNullToUndefined(workspaceConfig.pipelineGitRemoteName),
featureFlags: turnNullToUndefined(workspaceConfig.featureFlags),
......
import * as temp from 'temp';
import * as vscode from 'vscode';
import simpleGit, { SimpleGit } from 'simple-git';
import { getInstanceUrl } from './get_instance_url';
import { tokenService } from '../services/token_service';
import { GITLAB_COM_URL } from '../constants';
import { gitExtensionWrapper } from '../git/git_extension_wrapper';
describe('get_instance_url', () => {
const ORIGIN = 'origin';
const SECOND_REMOTE = 'second'; // name is important, we need this remote to be alphabetically behind origin
let repositoryRoot: string;
let git: SimpleGit;
temp.track(); // clean temporary folders after the tests finish
const createTempFolder = (): Promise<string> =>
new Promise((resolve, reject) => {
temp.mkdir('vscodeWorkplace', (err, dirPath) => {
if (err) reject(err);
resolve(dirPath);
});
});
beforeEach(() => {
jest.spyOn(gitExtensionWrapper, 'gitBinaryPath', 'get').mockReturnValue('git');
});
it('returns configured instanceUrl if the config contains one', async () => {
(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
instanceUrl: 'https://test.com',
});
expect(await getInstanceUrl()).toBe('https://test.com');
});
it('returns default instanceUrl when there is no configuration', async () => {
(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({});
expect(await getInstanceUrl()).toBe(GITLAB_COM_URL);
});
describe('heuristic', () => {
let tokens = {};
const fakeContext = {
globalState: {
get: () => tokens,
},
};
beforeEach(async () => {
repositoryRoot = await createTempFolder();
git = simpleGit(repositoryRoot, { binary: 'git' });
await git.init();
await git.addRemote(ORIGIN, 'https://git@gitlab.com/gitlab-org/gitlab-vscode-extension.git');
tokens = {};
tokenService.init((fakeContext as any) as vscode.ExtensionContext);
});
it('returns instanceUrl when there is exactly one match between remotes and token URLs', async () => {
await git.addRemote(SECOND_REMOTE, 'https://git@test-instance.com/g/extension.git');
tokens = {
'https://test-instance.com': 'abc',
};
expect(await getInstanceUrl(repositoryRoot)).toBe('https://test-instance.com');
});
it('returns default instanceUrl when there is multiple matches between remotes and token URLs', async () => {
await git.addRemote(SECOND_REMOTE, 'https://git@test-instance.com/g/extension.git');
tokens = {
'https://test-instance.com': 'abc',
'https://gitlab.com': 'def',
};
expect(await getInstanceUrl(repositoryRoot)).toBe(GITLAB_COM_URL);
});
it('it works with URLs in git format', async () => {
await git.addRemote(SECOND_REMOTE, 'git@test-instance.com:g/extension.git');
tokens = {
'https://test-instance.com': 'abc',
};
expect(await getInstanceUrl(repositoryRoot)).toBe('https://test-instance.com');
});
});
});
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';
import { parseGitRemote } from '../git/git_remote_parser';
import * as assert from 'assert';
import { gitExtensionWrapper } from '../git/git_extension_wrapper';
async function fetch(cmd: string, repositoryRoot: string): Promise<string | null> {
const [, ...args] = cmd.trim().split(' ');
const { stdout } = await execa(gitExtensionWrapper.gitBinaryPath, args, {
cwd: repositoryRoot,
preferLocal: false,
});
return stdout;
}
async function fetchGitRemoteUrls(repositoryRoot: string): Promise<string[]> {
const fetchGitRemotesVerbose = async (): Promise<string[]> => {
const output = await fetch('git remote -v', repositoryRoot);
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(repositoryRoot: string) {
const uriHostname = (uri: string) => parseGitRemote(uri)?.host;
const instanceUrls = tokenService.getInstanceUrls();
const gitRemotes = await fetchGitRemoteUrls(repositoryRoot);
const gitRemoteHosts = gitRemotes.map(uriHostname);
return instanceUrls.filter(instanceUrl =>
gitRemoteHosts.includes(url.parse(instanceUrl).host || undefined),
);
}
async function heuristicInstanceUrl(repositoryRoot: string) {
// if the intersection of git remotes and configured PATs exists and is exactly
// one hostname, use it
const intersection = await intersectionOfInstanceAndTokenUrls(repositoryRoot);
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(repositoryRoot?: string): Promise<string> {
// FIXME: if you are touching this configuration statement, move the configuration to get_extension_configuration.ts
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 (repositoryRoot) {
// try to determine the instance URL heuristically
const heuristicUrl = await heuristicInstanceUrl(repositoryRoot);
if (heuristicUrl) {
return heuristicUrl;
}
}
// default to Gitlab cloud
return GITLAB_COM_URL;
export function getInstanceUrl(repositoryRoot: string): string {
const repository = gitExtensionWrapper.getRepository(repositoryRoot);
assert(repository, `${repositoryRoot} doesn't contain a git repository`);
return repository.instanceUrl;
}
......@@ -57,7 +57,7 @@ describe('MR Review', () => {
beforeEach(() => {
server.resetHandlers();
dataProvider = new IssuableDataProvider();
mrItemModel = new MrItemModel(openMergeRequestResponse, getRepositoryRoot());
mrItemModel = new MrItemModel(openMergeRequestResponse, { uri: getRepositoryRoot() });
});
after(async () => {
......
......@@ -8,6 +8,7 @@ const gitLabService = require('../../src/gitlab_service');
const { GitLabNewService } = require('../../src/gitlab/gitlab_new_service');
const snippetsResponse = require('./fixtures/graphql/snippets.json');
const packageJson = require('../../package.json');
const { getRepositoryRoot } = require('./test_infrastructure/helpers');
const validateUserAgent = req => {
const userAgent = req.headers.get('User-Agent');
......@@ -50,7 +51,7 @@ describe('User-Agent header', () => {
});
it('is sent with requests from GitLabService', async () => {
await gitLabService.fetchCurrentUser();
await gitLabService.fetchCurrentUser(getRepositoryRoot());
validateUserAgent(capturedRequest);
});
......
......@@ -80,7 +80,7 @@ describe('GitLab webview', () => {
replacePanelEventSystem();
webviewPanel = await webviewController.create(
openIssueResponse,
vscode.workspace.workspaceFolders[0],
vscode.workspace.workspaceFolders[0].uri.fsPath,
);
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册