Commit c5d0e897 authored by Lennard Sprong's avatar Lennard Sprong
Browse files

feat: Display user avatars in commit history

parent aa5dba6d
Loading
Loading
Loading
Loading
+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 },
});
+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(() => {
@@ -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);
  });
});
+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,
@@ -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() {
+4 −0
Original line number Diff line number Diff line
@@ -206,3 +206,7 @@ interface RestProtectedBranch {
    user_id: number | null;
  }[];
}

interface RestAvatar {
  avatar_url: string;
}