Loading src/common/gitlab/api/get_avatar.ts 0 → 100644 +8 −0 Original line number Diff line number Diff line import { GetRequest } from '../../platform/web_ide'; export const avatarRequest = (email: string): GetRequest<RestAvatar> => ({ type: 'rest', method: 'GET', path: '/avatar', searchParams: { email }, }); src/desktop/gitlab/gitlab_history_provider.test.ts +36 −0 Original line number Diff line number Diff line import { createFakePartial } from '../../common/test_utils/create_fake_partial'; import { projectInRepository } from '../test_utils/entities'; import type { GetRequest } from '../../common/platform/web_ide'; import { getGitLabService } from './get_gitlab_service'; import { gitlabHistoryProvider } from './gitlab_history_provider'; import { getProjectRepository, GitLabProjectRepository } from './gitlab_project_repository'; import { GitLabService } from './gitlab_service'; jest.mock('../gitlab/gitlab_project_repository'); jest.mock('../gitlab/get_gitlab_service'); describe('gitlabHistoryProvider', () => { let fakeFetch: jest.Mock; beforeEach(() => { jest.mocked(getProjectRepository).mockReturnValue( createFakePartial<GitLabProjectRepository>({ getSelectedOrDefaultForRepository: jest.fn().mockReturnValue(projectInRepository), }), ); fakeFetch = jest.fn().mockImplementation(async (request: GetRequest<RestAvatar>) => { const { email } = request.searchParams ?? {}; return { avatar_url: `avatar for ${email}` }; }); jest.mocked(getGitLabService).mockReturnValue( createFakePartial<GitLabService>({ fetchFromApi: fakeFetch, }), ); }); afterEach(() => { Loading @@ -23,4 +38,25 @@ describe('gitlabHistoryProvider', () => { const result = await gitlabHistoryProvider.provideHoverCommands(repository); expect(result).toHaveLength(1); }); it('provides avatars', async () => { const repository = projectInRepository.pointer.repository.rawRepository; const result = await gitlabHistoryProvider.provideAvatar(repository, { commits: [ { hash: 'commit1', authorEmail: 'first@gitlab.com' }, { hash: 'commit2', authorEmail: 'second@gitlab.com' }, { hash: 'commit3', authorEmail: 'first@gitlab.com' }, ], size: 36, }); expect(result!.size).toBe(3); expect(result!.get('commit1')).toBe('avatar for first@gitlab.com'); expect(result!.get('commit2')).toBe('avatar for second@gitlab.com'); expect(result!.get('commit3')).toBe('avatar for first@gitlab.com'); // Check for single fetch for each distinct e-mail address expect(fakeFetch).toHaveBeenCalledTimes(2); }); }); src/desktop/gitlab/gitlab_history_provider.ts +45 −4 Original line number Diff line number Diff line import { Command } from 'vscode'; import type { Repository, SourceControlHistoryItemDetailsProvider } from '../api/git'; import type { Command, ProviderResult } from 'vscode'; import type { AvatarQuery, Repository, SourceControlHistoryItemDetailsProvider } from '../api/git'; import { avatarRequest } from '../../common/gitlab/api/get_avatar'; import { getProjectRepository } from './gitlab_project_repository'; import { getGitLabService } from './get_gitlab_service'; class GitlabHistoryProvider implements SourceControlHistoryItemDetailsProvider { #avatarCache: Record<string, ProviderResult<string>> = {}; async provideHoverCommands(repository: Repository): Promise<Command[] | undefined> { const projectInRepository = getProjectRepository().getSelectedOrDefaultForRepository( repository.rootUri.fsPath, Loading @@ -23,8 +27,45 @@ class GitlabHistoryProvider implements SourceControlHistoryItemDetailsProvider { ]; } provideAvatar() { return undefined; async provideAvatar( repository: Repository, query: AvatarQuery, ): Promise<Map<string, string | undefined> | undefined> { const projectInRepository = getProjectRepository().getSelectedOrDefaultForRepository( repository.rootUri.fsPath, ); if (!projectInRepository) return undefined; const gitlabService = getGitLabService(projectInRepository); const map = new Map(); await Promise.all( query.commits.map(async commit => { const { hash, authorEmail } = commit; if (!authorEmail) { return; } if (authorEmail in this.#avatarCache) { const url = await this.#avatarCache[authorEmail]; if (url) map.set(hash, url); } else { const promise = gitlabService .fetchFromApi(avatarRequest(authorEmail)) .then(result => result.avatar_url) .catch(() => null); // Add the Promise to the cache, so other items don't do another request for the same e-mail address. this.#avatarCache[authorEmail] = promise; const url = await promise; if (url) map.set(hash, url); // Remove the Promise and replace it with the end result this.#avatarCache[authorEmail] = url; } }), ); return map; } provideMessageLinks() { Loading src/desktop/types.d.ts +4 −0 Original line number Diff line number Diff line Loading @@ -206,3 +206,7 @@ interface RestProtectedBranch { user_id: number | null; }[]; } interface RestAvatar { avatar_url: string; } Loading
src/common/gitlab/api/get_avatar.ts 0 → 100644 +8 −0 Original line number Diff line number Diff line import { GetRequest } from '../../platform/web_ide'; export const avatarRequest = (email: string): GetRequest<RestAvatar> => ({ type: 'rest', method: 'GET', path: '/avatar', searchParams: { email }, });
src/desktop/gitlab/gitlab_history_provider.test.ts +36 −0 Original line number Diff line number Diff line import { createFakePartial } from '../../common/test_utils/create_fake_partial'; import { projectInRepository } from '../test_utils/entities'; import type { GetRequest } from '../../common/platform/web_ide'; import { getGitLabService } from './get_gitlab_service'; import { gitlabHistoryProvider } from './gitlab_history_provider'; import { getProjectRepository, GitLabProjectRepository } from './gitlab_project_repository'; import { GitLabService } from './gitlab_service'; jest.mock('../gitlab/gitlab_project_repository'); jest.mock('../gitlab/get_gitlab_service'); describe('gitlabHistoryProvider', () => { let fakeFetch: jest.Mock; beforeEach(() => { jest.mocked(getProjectRepository).mockReturnValue( createFakePartial<GitLabProjectRepository>({ getSelectedOrDefaultForRepository: jest.fn().mockReturnValue(projectInRepository), }), ); fakeFetch = jest.fn().mockImplementation(async (request: GetRequest<RestAvatar>) => { const { email } = request.searchParams ?? {}; return { avatar_url: `avatar for ${email}` }; }); jest.mocked(getGitLabService).mockReturnValue( createFakePartial<GitLabService>({ fetchFromApi: fakeFetch, }), ); }); afterEach(() => { Loading @@ -23,4 +38,25 @@ describe('gitlabHistoryProvider', () => { const result = await gitlabHistoryProvider.provideHoverCommands(repository); expect(result).toHaveLength(1); }); it('provides avatars', async () => { const repository = projectInRepository.pointer.repository.rawRepository; const result = await gitlabHistoryProvider.provideAvatar(repository, { commits: [ { hash: 'commit1', authorEmail: 'first@gitlab.com' }, { hash: 'commit2', authorEmail: 'second@gitlab.com' }, { hash: 'commit3', authorEmail: 'first@gitlab.com' }, ], size: 36, }); expect(result!.size).toBe(3); expect(result!.get('commit1')).toBe('avatar for first@gitlab.com'); expect(result!.get('commit2')).toBe('avatar for second@gitlab.com'); expect(result!.get('commit3')).toBe('avatar for first@gitlab.com'); // Check for single fetch for each distinct e-mail address expect(fakeFetch).toHaveBeenCalledTimes(2); }); });
src/desktop/gitlab/gitlab_history_provider.ts +45 −4 Original line number Diff line number Diff line import { Command } from 'vscode'; import type { Repository, SourceControlHistoryItemDetailsProvider } from '../api/git'; import type { Command, ProviderResult } from 'vscode'; import type { AvatarQuery, Repository, SourceControlHistoryItemDetailsProvider } from '../api/git'; import { avatarRequest } from '../../common/gitlab/api/get_avatar'; import { getProjectRepository } from './gitlab_project_repository'; import { getGitLabService } from './get_gitlab_service'; class GitlabHistoryProvider implements SourceControlHistoryItemDetailsProvider { #avatarCache: Record<string, ProviderResult<string>> = {}; async provideHoverCommands(repository: Repository): Promise<Command[] | undefined> { const projectInRepository = getProjectRepository().getSelectedOrDefaultForRepository( repository.rootUri.fsPath, Loading @@ -23,8 +27,45 @@ class GitlabHistoryProvider implements SourceControlHistoryItemDetailsProvider { ]; } provideAvatar() { return undefined; async provideAvatar( repository: Repository, query: AvatarQuery, ): Promise<Map<string, string | undefined> | undefined> { const projectInRepository = getProjectRepository().getSelectedOrDefaultForRepository( repository.rootUri.fsPath, ); if (!projectInRepository) return undefined; const gitlabService = getGitLabService(projectInRepository); const map = new Map(); await Promise.all( query.commits.map(async commit => { const { hash, authorEmail } = commit; if (!authorEmail) { return; } if (authorEmail in this.#avatarCache) { const url = await this.#avatarCache[authorEmail]; if (url) map.set(hash, url); } else { const promise = gitlabService .fetchFromApi(avatarRequest(authorEmail)) .then(result => result.avatar_url) .catch(() => null); // Add the Promise to the cache, so other items don't do another request for the same e-mail address. this.#avatarCache[authorEmail] = promise; const url = await promise; if (url) map.set(hash, url); // Remove the Promise and replace it with the end result this.#avatarCache[authorEmail] = url; } }), ); return map; } provideMessageLinks() { Loading
src/desktop/types.d.ts +4 −0 Original line number Diff line number Diff line Loading @@ -206,3 +206,7 @@ interface RestProtectedBranch { user_id: number | null; }[]; } interface RestAvatar { avatar_url: string; }