Browse Source

Fixes avatar urls for GitHub Enterprise

main
Eric Amodio 2 years ago
parent
commit
a9e06b203b
5 changed files with 109 additions and 13 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -1
      src/git/remotes/github.ts
  3. +85
    -10
      src/plus/github/github.ts
  4. +2
    -2
      src/plus/gitlab/gitlab.ts
  5. +20
    -0
      src/system/version.ts

+ 1
- 0
CHANGELOG.md View File

@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
![Pull requests on line annotation and hovers](https://raw.githubusercontent.com/gitkraken/vscode-gitlens/main/images/docs/hovers-current-line-details.png)
- Adds associated pull request to status bar blame
![Pull requests on status bar](https://raw.githubusercontent.com/gitkraken/vscode-gitlens/main/images/docs/status-bar.png)
- Adds GitHub avatars
- Adds associated pull requests to branches and commits in GitLens views
- Adds rich autolinks for GitHub issues and merge requests, including titles, status, and authors
- Adds rich support to _Autolinked Issues and Pull Requests_ within comparisons to list autolinked GitHub issues and merge requests in commit messages

+ 1
- 1
src/git/remotes/github.ts View File

@ -35,7 +35,7 @@ export class GitHubRemote extends RichRemoteProvider {
}
get apiBaseUrl() {
return this.custom ? `${this.protocol}://${this.domain}/api` : `https://api.${this.domain}`;
return this.custom ? `${this.protocol}://${this.domain}/api/v3` : `https://${this.domain}/api/v3`;
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;

+ 85
- 10
src/plus/github/github.ts View File

@ -3,7 +3,7 @@ import { GraphqlResponseError } from '@octokit/graphql';
import { RequestError } from '@octokit/request-error';
import type { Endpoints, OctokitResponse, RequestParameters } from '@octokit/types';
import type { HttpsProxyAgent } from 'https-proxy-agent';
import { Disposable, Event, EventEmitter, window } from 'vscode';
import { Disposable, Event, EventEmitter, Uri, window } from 'vscode';
import { fetch, getProxyAgent, wrapForForcedInsecureSSL } from '@env/fetch';
import { isWeb } from '@env/platform';
import { configuration } from '../../configuration';
@ -22,6 +22,7 @@ import type { RichRemoteProvider } from '../../git/remotes/provider';
import { LogCorrelationContext, Logger, LogLevel } from '../../logger';
import { debug } from '../../system/decorators/log';
import { Stopwatch } from '../../system/stopwatch';
import { fromString, satisfies, Version } from '../../system/version';
import {
GitHubBlame,
GitHubBlameRange,
@ -51,17 +52,13 @@ export class GitHubApi implements Disposable {
constructor(_container: Container) {
this._disposable = Disposable.from(
configuration.onDidChange(e => {
if (configuration.changed(e, 'proxy')) {
this._proxyAgent = null;
this._octokits.clear();
} else if (configuration.changed(e, 'outputLevel')) {
this._octokits.clear();
if (configuration.changed(e, 'proxy') || configuration.changed(e, 'outputLevel')) {
this.resetCaches();
}
}),
configuration.onDidChangeAny(e => {
if (e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.proxyStrictSSL')) {
this._proxyAgent = null;
this._octokits.clear();
this.resetCaches();
}
}),
);
@ -71,6 +68,12 @@ export class GitHubApi implements Disposable {
this._disposable?.dispose();
}
private resetCaches(): void {
this._proxyAgent = null;
this._octokits.clear();
this._enterpriseVersions.clear();
}
private _proxyAgent: HttpsProxyAgent | null | undefined = null;
private get proxyAgent(): HttpsProxyAgent | undefined {
if (isWeb) return undefined;
@ -147,7 +150,13 @@ export class GitHubApi implements Disposable {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
// If we are GitHub Enterprise, we may need to convert the avatar URL since it might require authentication
avatarUrl:
!author.avatarUrl || isGitHubDotCom(options)
? author.avatarUrl ?? undefined
: author.email && options?.baseUrl != null
? await this.createEnterpriseAvatarUrl(token, options.baseUrl, author.email, options.avatarSize)
: undefined,
};
} catch (ex) {
if (ex instanceof ProviderRequestNotFoundError) return undefined;
@ -216,7 +225,13 @@ export class GitHubApi implements Disposable {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
// If we are GitHub Enterprise, we may need to convert the avatar URL since it might require authentication
avatarUrl:
!author.avatarUrl || isGitHubDotCom(options)
? author.avatarUrl ?? undefined
: author.email && options?.baseUrl != null
? await this.createEnterpriseAvatarUrl(token, options.baseUrl, author.email, options.avatarSize)
: undefined,
};
} catch (ex) {
if (ex instanceof ProviderRequestNotFoundError) return undefined;
@ -1715,6 +1730,27 @@ export class GitHubApi implements Disposable {
}
}
private _enterpriseVersions = new Map<string, Version | null>();
@debug<GitHubApi['getEnterpriseVersion']>({ args: { 0: '<token>' } })
private async getEnterpriseVersion(token: string, options?: { baseUrl?: string }): Promise<Version | undefined> {
let version = this._enterpriseVersions.get(token);
if (version != null) return version;
if (version === null) return undefined;
try {
const rsp = await this.request(undefined, token, 'GET /meta', options);
const v = (rsp?.data as any)?.installed_version as string | null | undefined;
version = v ? fromString(v) : null;
} catch (ex) {
debugger;
version = null;
}
this._enterpriseVersions.set(token, version);
return version ?? undefined;
}
private _octokits = new Map<string, Octokit>();
private octokit(token: string, options?: ConstructorParameters<typeof Octokit>[0]): Octokit {
let octokit = this._octokits.get(token);
@ -1915,4 +1951,43 @@ export class GitHubApi implements Disposable {
void window.showErrorMessage(ex.message);
}
}
private async createEnterpriseAvatarUrl(
token: string,
baseUrl: string,
email: string,
avatarSize: number | undefined,
): Promise<string | undefined> {
avatarSize = avatarSize ?? 16;
let avatarEndpointUrl = `https://avatars.githubusercontent.com`;
const version = await this.getEnterpriseVersion(token, { baseUrl: baseUrl });
if (satisfies(version, '>= 3.0.0')) {
avatarEndpointUrl = `${baseUrl}/enterprise/avatars`;
const match = /^(?:(\d+)\+)?(.+?)@users\.noreply\.(.*)$/i.exec(email);
if (match != null) {
const uri = Uri.parse(baseUrl);
const [, userId, login, authority] = match;
if (uri.authority === authority) {
if (userId != null) {
return `${avatarEndpointUrl}/u/${encodeURIComponent(userId)}?s=${avatarSize}`;
}
if (login != null) {
return `${avatarEndpointUrl}/${encodeURIComponent(login)}?s=${avatarSize}`;
}
}
}
}
// The /u/e endpoint automatically falls back to gravatar if not found
return `${avatarEndpointUrl}/u/e?email=${encodeURIComponent(email)}&s=${avatarSize}`;
}
}
function isGitHubDotCom(options?: { baseUrl?: string }) {
return options?.baseUrl == null || options.baseUrl === 'https://api.github.com';
}

+ 2
- 2
src/plus/gitlab/gitlab.ts View File

@ -663,7 +663,7 @@ $search: String!
rsp = await wrapForForcedInsecureSSL(provider.getIgnoreSSLErrors(), () =>
fetch(`${baseUrl ?? 'https://gitlab.com/api'}/graphql`, {
method: 'POST',
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
agent: agent,
body: JSON.stringify({ query: query, variables: variables }),
}),
@ -715,7 +715,7 @@ $search: String!
try {
rsp = await wrapForForcedInsecureSSL(provider.getIgnoreSSLErrors(), () =>
fetch(url, {
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
agent: agent,
...options,
}),

+ 20
- 0
src/system/version.ts View File

@ -48,3 +48,23 @@ export function fromString(version: string): Version {
const [major, minor, patch] = ver.split('.');
return from(major, minor, patch, pre);
}
export function satisfies(v: string | Version | null | undefined, requirement: string): boolean {
if (v == null) return false;
const [op, version] = requirement.split(' ');
if (op === '=') {
return compare(v, version) === 0;
} else if (op === '>') {
return compare(v, version) > 0;
} else if (op === '>=') {
return compare(v, version) >= 0;
} else if (op === '<') {
return compare(v, version) < 0;
} else if (op === '<=') {
return compare(v, version) <= 0;
}
throw new Error(`Unknown operator: ${op}`);
}

Loading…
Cancel
Save