Переглянути джерело

Adds provider-based avatar support

main
Eric Amodio 4 роки тому
джерело
коміт
d43571779d
20 змінених файлів з 582 додано та 108 видалено
  1. +6
    -4
      src/annotations/gutterBlameAnnotationProvider.ts
  2. +199
    -15
      src/avatars.ts
  3. +1
    -0
      src/constants.ts
  4. +1
    -1
      src/git/formatters/commitFormatter.ts
  5. +2
    -0
      src/git/gitService.ts
  6. +7
    -0
      src/git/models/author.ts
  7. +10
    -2
      src/git/models/commit.ts
  8. +10
    -2
      src/git/models/contributor.ts
  9. +1
    -0
      src/git/models/models.ts
  10. +29
    -1
      src/git/remotes/github.ts
  11. +71
    -4
      src/git/remotes/provider.ts
  12. +217
    -68
      src/github/github.ts
  13. +4
    -0
      src/system/string.ts
  14. +7
    -3
      src/views/contributorsView.ts
  15. +0
    -1
      src/views/nodes/branchNode.ts
  16. +1
    -1
      src/views/nodes/commitFileNode.ts
  17. +2
    -2
      src/views/nodes/commitNode.ts
  18. +4
    -2
      src/views/nodes/contributorNode.ts
  19. +4
    -0
      src/views/nodes/contributorsNode.ts
  20. +6
    -2
      src/webviews/rebaseEditor.ts

+ 6
- 4
src/annotations/gutterBlameAnnotationProvider.ts Переглянути файл

@ -144,7 +144,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
decorationOptions.push(gutter);
if (avatars && commit.email != null) {
this.applyAvatarDecoration(commit, gutter, gravatarDefault, avatarDecorationsMap!);
await this.applyAvatarDecoration(commit, gutter, gravatarDefault, avatarDecorationsMap!);
}
decorationsMap.set(l.sha, gutter);
@ -202,7 +202,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
this.editor.setDecorations(Decorations.gutterBlameHighlight, highlightDecorationRanges);
}
private applyAvatarDecoration(
private async applyAvatarDecoration(
commit: GitBlameCommit,
gutter: DecorationOptions,
gravatarDefault: GravatarDefaultStyle,
@ -211,10 +211,12 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
let avatarDecoration = map.get(commit.email!);
if (avatarDecoration == null) {
avatarDecoration = {
contentIconPath: commit.getAvatarUri(gravatarDefault),
contentText: '',
height: '16px',
width: '16px',
textDecoration: 'none;position:absolute;top:1px;left:5px',
textDecoration: `none;position:absolute;top:1px;left:5px;background:url(${(
await commit.getAvatarUri(true, { fallback: gravatarDefault })
).toString()});background-size:16px 16px`,
};
map.set(commit.email!, avatarDecoration);
}

+ 199
- 15
src/avatars.ts Переглянути файл

@ -1,20 +1,75 @@
'use strict';
import * as fs from 'fs';
import { Uri } from 'vscode';
import { EventEmitter, Uri } from 'vscode';
import { GravatarDefaultStyle } from './config';
import { Strings } from './system';
import { ContactPresenceStatus } from './vsls/vsls';
import { WorkspaceState } from './constants';
import { Container } from './container';
import { GitRevisionReference } from './git/git';
import { Functions, Strings } from './system';
import { ContactPresenceStatus } from './vsls/vsls';
// TODO@eamodio Use timestamp
// TODO@eamodio Clear avatar cache on remote / provider connection change
interface Avatar<T = Uri> {
uri?: T | null;
fallback: T;
timestamp: number;
// TODO@eamodio Add a fail count, to avoid failing on a single failure
}
type SerializedAvatar = Avatar<string>;
let avatarCache: Map<string, Avatar> | undefined;
const avatarQueue = new Map<string, Promise<Uri | undefined> | null>();
const avatarCache = new Map<string, Uri>();
const missingGravatarHash = '00000000000000000000000000000000';
const presenceCache = new Map<ContactPresenceStatus, string>();
const gitHubNoReplyAddressRegex = /^(?:(?<userId>\d+)\+)?(?<userName>[a-zA-Z\d-]{1,39})@users\.noreply\.github\.com$/;
const _onDidFetchAvatar = new EventEmitter<{ email: string }>();
export const onDidFetchAvatar = _onDidFetchAvatar.event;
onDidFetchAvatar(
Functions.debounce(() => {
void Container.context.workspaceState.update(
WorkspaceState.Avatars,
avatarCache == null
? undefined
: [...avatarCache.entries()].map<[string, SerializedAvatar]>(([key, value]) => [
key,
{
uri: value.uri != null ? value.uri.toString() : value.uri,
fallback: value.fallback.toString(),
timestamp: value.timestamp,
},
]),
);
}, 5000),
);
export function clearAvatarCache() {
avatarCache.clear();
avatarCache?.clear();
avatarQueue.clear();
void Container.context.workspaceState.update(WorkspaceState.Avatars, undefined);
}
function ensureAvatarCache(cache: Map<string, Avatar> | undefined): asserts cache is Map<string, Avatar> {
if (cache == null) {
const avatars: [string, Avatar][] | undefined = Container.context.workspaceState
.get<[string, SerializedAvatar][]>(WorkspaceState.Avatars)
?.map<[string, Avatar]>(([key, value]) => [
key,
{
uri: value.uri != null ? Uri.parse(value.uri) : value.uri,
fallback: Uri.parse(value.fallback),
timestamp: value.timestamp,
},
]);
avatarCache = new Map<string, Avatar>(avatars);
}
}
function getAvatarUriFromGitHubNoReplyAddress(email: string | undefined, size: number = 16): Uri | undefined {
@ -27,25 +82,154 @@ function getAvatarUriFromGitHubNoReplyAddress(email: string | undefined, size: n
return Uri.parse(`https://avatars.githubusercontent.com/${userId ? `u/${userId}` : userName}?size=${size}`);
}
export function getAvatarUri(email: string | undefined, fallback: GravatarDefaultStyle, size: number = 16): Uri {
const hash =
email != null && email.length !== 0 ? Strings.md5(email.trim().toLowerCase(), 'hex') : missingGravatarHash;
export function getAvatarUri(
email: string | undefined,
repoPathOrCommit: string | GitRevisionReference | undefined,
wait: false,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Uri;
export function getAvatarUri(
email: string | undefined,
repoPathOrCommit: string | GitRevisionReference | undefined,
wait: true,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Promise<Uri>;
export function getAvatarUri(
email: string | undefined,
repoPathOrCommit: string | GitRevisionReference | undefined,
wait: boolean,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Uri | Promise<Uri>;
export function getAvatarUri(
email: string | undefined,
repoPathOrCommit: string | GitRevisionReference | undefined,
wait: boolean,
{ fallback, listener, size = 16 }: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number } = {},
): Uri | Promise<Uri> {
ensureAvatarCache(avatarCache);
if (email == null || email.length === 0) {
const key = `${missingGravatarHash}:${size}`;
let avatar = avatarCache.get(key);
if (avatar == null) {
avatar = {
fallback: Uri.parse(
`https://www.gravatar.com/avatar/${missingGravatarHash}.jpg?s=${size}&d=${fallback}`,
),
timestamp: Date.now(),
};
avatarCache.set(key, avatar);
}
return avatar.uri ?? avatar.fallback;
}
const hash = Strings.md5(email.trim().toLowerCase(), 'hex');
const key = `${hash}:${size}`;
let avatar = avatarCache.get(key);
if (avatar !== undefined) return avatar;
if (avatar == null) {
avatar = {
fallback:
getAvatarUriFromGitHubNoReplyAddress(email, size) ??
Uri.parse(`https://www.gravatar.com/avatar/${hash}.jpg?s=${size}&d=${fallback}`),
timestamp: Date.now(),
};
avatarCache.set(key, avatar);
}
avatar =
getAvatarUriFromGitHubNoReplyAddress(email, size) ??
Uri.parse(`https://www.gravatar.com/avatar/${hash}.jpg?s=${size}&d=${fallback}`);
avatarCache.set(key, avatar);
let query = avatarQueue.get(key);
if (query == null && avatar.uri === undefined && repoPathOrCommit != null) {
query = getRemoteProviderAvatarUri(key, email, repoPathOrCommit, avatar.fallback, { size: size });
avatarQueue.set(key, query);
return avatar;
void signalOnAvatarQueryComplete(email, query, listener, true);
} else if (query != null) {
void signalOnAvatarQueryComplete(email, query, listener, false);
}
if (wait && query != null) {
return query.then(value => value ?? avatar!.uri ?? avatar!.fallback);
}
return avatar.uri ?? avatar.fallback;
}
async function getRemoteProviderAvatarUri(
key: string,
email: string,
repoPathOrCommit: string | GitRevisionReference,
fallback: Uri,
{ size = 16 }: { size?: number } = {},
) {
ensureAvatarCache(avatarCache);
try {
let account;
// if (typeof repoPathOrCommit === 'string') {
// const remote = await Container.git.getRemoteWithApiProvider(repoPathOrCommit);
// account = await remote?.provider.getAccountForEmail(email, { avatarSize: size });
// } else {
if (typeof repoPathOrCommit !== 'string') {
const remote = await Container.git.getRemoteWithApiProvider(repoPathOrCommit.repoPath);
account = await remote?.provider.getAccountForCommit(repoPathOrCommit.ref, { avatarSize: size });
}
if (account == null) {
avatarCache.set(key, { uri: null, fallback: fallback, timestamp: Date.now() });
return undefined;
}
const uri = Uri.parse(account.avatarUrl);
avatarCache.set(key, { uri: uri, fallback: fallback, timestamp: Date.now() });
if (account.email != null && Strings.equalsIgnoreCase(email, account.email)) {
avatarCache.set(`${Strings.md5(account.email.trim().toLowerCase(), 'hex')}:${size}`, {
uri: uri,
fallback: fallback,
timestamp: Date.now(),
});
}
return uri;
} catch {
avatarCache.set(key, { uri: null, fallback: fallback, timestamp: Date.now() });
return undefined;
} finally {
avatarQueue.delete(key);
}
}
async function signalOnAvatarQueryComplete(
email: string,
query: Promise<Uri | undefined>,
listener: (() => void) | undefined,
fire: boolean,
) {
if (listener == null) {
if (fire) {
_onDidFetchAvatar.fire({ email: email });
}
return;
}
const disposable = onDidFetchAvatar(listener);
try {
await query;
if (fire) {
_onDidFetchAvatar.fire({ email: email });
}
} finally {
disposable.dispose();
}
}
export function getPresenceDataUri(status: ContactPresenceStatus) {
let dataUri = presenceCache.get(status);
if (dataUri === undefined) {
if (dataUri == null) {
const contents = fs
.readFileSync(Container.context.asAbsolutePath(`images/dark/icon-presence-${status}.svg`))
.toString('base64');

+ 1
- 0
src/constants.ts Переглянути файл

@ -182,6 +182,7 @@ export interface StarredRepositories {
}
export enum WorkspaceState {
Avatars = 'gitlens:avatars',
BranchComparisons = 'gitlens:branch:comparisons',
DefaultRemote = 'gitlens:remote:default',
PinnedComparisons = 'gitlens:pinned:comparisons',

+ 1
- 1
src/git/formatters/commitFormatter.ts Переглянути файл

@ -214,7 +214,7 @@ export class CommitFormatter extends Formatter {
private _getAvatarMarkdown(title: string) {
const size = Container.config.hovers.avatarSize;
return `![${title}](${this._item
.getAvatarUri(Container.config.defaultGravatarsStyle, size)
.getAvatarUri(false, { fallback: Container.config.defaultGravatarsStyle, size: size })
.toString(true)}|width=${size},height=${size} "${title}")`;
}

+ 2
- 0
src/git/gitService.ts Переглянути файл

@ -2799,6 +2799,8 @@ export class GitService implements Disposable {
): Promise<GitRemote<RemoteProviderWithApi> | undefined> {
if (remotesOrRepoPath == null) return undefined;
// TODO@eamodio Add caching to avoid constant lookups
const remotes = (typeof remotesOrRepoPath === 'string'
? await this.getRemotes(remotesOrRepoPath)
: remotesOrRepoPath

+ 7
- 0
src/git/models/author.ts Переглянути файл

@ -0,0 +1,7 @@
'use strict';
export interface Account {
provider: string;
name: string | undefined;
email: string | undefined;
avatarUrl: string;
}

+ 10
- 2
src/git/models/commit.ts Переглянути файл

@ -225,8 +225,16 @@ export abstract class GitCommit implements GitRevisionReference {
return GitUri.getFormattedPath(this.fileName, options);
}
getAvatarUri(fallback: GravatarDefaultStyle, size: number = 16): Uri {
return getAvatarUri(this.email, fallback, size);
getAvatarUri(wait: false, options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number }): Uri;
getAvatarUri(
wait: true,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Promise<Uri>;
getAvatarUri(
wait: boolean,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Uri | Promise<Uri> {
return getAvatarUri(this.email, this, wait, options);
}
@memoize()

+ 10
- 2
src/git/models/contributor.ts Переглянути файл

@ -20,8 +20,16 @@ export class GitContributor {
public readonly current: boolean = false,
) {}
getAvatarUri(fallback: GravatarDefaultStyle, size: number = 16): Uri {
return getAvatarUri(this.email, fallback, size);
getAvatarUri(wait: false, options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number }): Uri;
getAvatarUri(
wait: true,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Promise<Uri>;
getAvatarUri(
wait: boolean,
options?: { fallback?: GravatarDefaultStyle; listener?: () => void; size?: number },
): Uri | Promise<Uri> {
return getAvatarUri(this.email, undefined /*this.repoPath*/, wait, options);
}
toCoauthor(): string {

+ 1
- 0
src/git/models/models.ts Переглянути файл

@ -309,6 +309,7 @@ export namespace GitReference {
}
}
export * from './author';
export * from './blame';
export * from './blameCommit';
export * from './branch';

+ 29
- 1
src/git/remotes/github.ts Переглянути файл

@ -5,7 +5,7 @@ import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { GitHubPullRequest } from '../../github/github';
import { IssueOrPullRequest } from '../models/issue';
import { GitRevision } from '../models/models';
import { Account, GitRevision } from '../models/models';
import { PullRequest, PullRequestState } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { RemoteProviderWithApi } from './provider';
@ -155,6 +155,34 @@ export class GitHubRemote extends RemoteProviderWithApi {
return `${this.baseUrl}?path=${fileName}${line}`;
}
protected async onGetAccountForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await Container.github)?.getAccountForCommit(this.name, accessToken, owner, repo, ref, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected async onGetAccountForEmail(
{ accessToken }: AuthenticationSession,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await Container.github)?.getAccountForEmail(this.name, accessToken, owner, repo, email, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected async onGetIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
id: string,

+ 71
- 4
src/git/remotes/provider.ts Переглянути файл

@ -15,10 +15,7 @@ import { AutolinkReference } from '../../config';
import { Container } from '../../container';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { IssueOrPullRequest } from '../models/issue';
import { GitLogCommit } from '../models/logCommit';
import { PullRequest, PullRequestState } from '../models/pullRequest';
import { Repository } from '../models/repository';
import { Account, GitLogCommit, IssueOrPullRequest, PullRequest, PullRequestState, Repository } from '../models/models';
import { debug, gate, log, Promises } from '../../system';
const _onDidChangeAuthentication = new EventEmitter<{ reason: 'connected' | 'disconnected'; key: string }>();
@ -305,6 +302,60 @@ export abstract class RemoteProviderWithApi extends RemoteProvider {
@gate()
@debug()
async getAccountForCommit(
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.onGetAccountForCommit(this._session!, ref, options);
this.invalidAuthenticationCount = 0;
return author;
} catch (ex) {
Logger.error(ex, cc);
if (ex instanceof AuthenticationError) {
this.handleAuthenticationException();
}
return undefined;
}
}
@gate()
@debug()
async getAccountForEmail(
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.onGetAccountForEmail(this._session!, email, options);
this.invalidAuthenticationCount = 0;
return author;
} catch (ex) {
Logger.error(ex, cc);
if (ex instanceof AuthenticationError) {
this.handleAuthenticationException();
}
return undefined;
}
}
@gate()
@debug()
async getIssueOrPullRequest(id: string): Promise<IssueOrPullRequest | undefined> {
const cc = Logger.getCorrelationContext();
@ -371,6 +422,22 @@ export abstract class RemoteProviderWithApi extends RemoteProvider {
protected abstract get authProvider(): { id: string; scopes: string[] };
protected abstract onGetAccountForCommit(
session: AuthenticationSession,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
protected abstract onGetAccountForEmail(
session: AuthenticationSession,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
protected abstract onGetIssueOrPullRequest(
session: AuthenticationSession,
id: string,

+ 217
- 68
src/github/github.ts Переглянути файл

@ -3,6 +3,7 @@ import { graphql } from '@octokit/graphql';
import { Logger } from '../logger';
import { debug } from '../system';
import { AuthenticationError, IssueOrPullRequest, PullRequest, PullRequestState } from '../git/git';
import { Account } from '../git/models/author';
export class GitHubApi {
@debug({
@ -10,6 +11,222 @@ export class GitHubApi {
1: _ => '<token>',
},
})
async getAccountForCommit(
provider: string,
token: string,
owner: string,
repo: string,
ref: string,
options?: {
baseUrl?: string;
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query ($owner: String!, $repo: String!, $ref: GitObjectID!, $avatarSize: Int) {
repository(name: $repo, owner: $owner) {
object(oid: $ref) {
... on Commit {
author {
name
email
avatarUrl(size: $avatarSize)
}
}
}
}
}`;
const rsp = await graphql<{
repository:
| {
object:
| {
author?: {
name: string | null;
email: string | null;
avatarUrl: string;
};
}
| null
| undefined;
}
| null
| undefined;
}>(query, {
owner: owner,
repo: repo,
ref: ref,
headers: { authorization: `Bearer ${token}` },
...options,
});
const author = rsp?.repository?.object?.author;
if (author == null) return undefined;
return {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
};
} catch (ex) {
Logger.error(ex, cc);
if (ex.code === 401) {
throw new AuthenticationError(ex);
}
throw ex;
}
}
@debug({
args: {
1: _ => '<token>',
},
})
async getAccountForEmail(
provider: string,
token: string,
owner: string,
repo: string,
email: string,
options?: {
baseUrl?: string;
avatarSize?: number;
},
): Promise<Account | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query ($emailQuery: String!, $avatarSize: Int) {
search(type: USER, query: $emailQuery, first: 1) {
nodes {
... on User {
name
email
avatarUrl(size: $avatarSize)
}
}
}
}`;
const rsp = await graphql<{
search:
| {
nodes:
| {
name: string | null;
email: string | null;
avatarUrl: string;
}[]
| null
| undefined;
}
| null
| undefined;
}>(query, {
owner: owner,
repo: repo,
emailQuery: `in:email ${email}`,
headers: { authorization: `Bearer ${token}` },
...options,
});
const author = rsp?.search?.nodes?.[0];
if (author == null) return undefined;
return {
provider: provider,
name: author.name ?? undefined,
email: author.email ?? undefined,
avatarUrl: author.avatarUrl,
};
} catch (ex) {
Logger.error(ex, cc);
if (ex.code === 401) {
throw new AuthenticationError(ex);
}
throw ex;
}
}
@debug({
args: {
1: _ => '<token>',
},
})
async getIssueOrPullRequest(
provider: string,
token: string,
owner: string,
repo: string,
number: number,
options?: {
baseUrl?: string;
},
): Promise<IssueOrPullRequest | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query pr($owner: String!, $repo: String!, $number: Int!) {
repository(name: $repo, owner: $owner) {
issueOrPullRequest(number: $number) {
__typename
... on Issue {
createdAt
closed
closedAt
title
}
... on PullRequest {
createdAt
closed
closedAt
title
}
}
}
}`;
const rsp = await graphql<{ repository?: { issueOrPullRequest?: GitHubIssueOrPullRequest } }>(query, {
owner: owner,
repo: repo,
number: number,
headers: { authorization: `Bearer ${token}` },
...options,
});
const issue = rsp?.repository?.issueOrPullRequest;
if (issue == null) return undefined;
return {
provider: provider,
type: issue.type,
id: number,
date: new Date(issue.createdAt),
title: issue.title,
closed: issue.closed,
closedDate: issue.closedAt == null ? undefined : new Date(issue.closedAt),
};
} catch (ex) {
Logger.error(ex, cc);
if (ex.code === 401) {
throw new AuthenticationError(ex);
}
throw ex;
}
}
@debug({
args: {
1: _ => '<token>',
},
})
async getPullRequestForBranch(
provider: string,
token: string,
@ -177,74 +394,6 @@ export class GitHubApi {
throw ex;
}
}
@debug({
args: {
1: _ => '<token>',
},
})
async getIssueOrPullRequest(
provider: string,
token: string,
owner: string,
repo: string,
number: number,
options?: {
baseUrl?: string;
},
): Promise<IssueOrPullRequest | undefined> {
const cc = Logger.getCorrelationContext();
try {
const query = `query pr($owner: String!, $repo: String!, $number: Int!) {
repository(name: $repo, owner: $owner) {
issueOrPullRequest(number: $number) {
__typename
... on Issue {
createdAt
closed
closedAt
title
}
... on PullRequest {
createdAt
closed
closedAt
title
}
}
}
}`;
const rsp = await graphql<{ repository?: { issueOrPullRequest?: GitHubIssueOrPullRequest } }>(query, {
owner: owner,
repo: repo,
number: number,
headers: { authorization: `Bearer ${token}` },
...options,
});
const issue = rsp?.repository?.issueOrPullRequest;
if (issue == null) return undefined;
return {
provider: provider,
type: issue.type,
id: number,
date: new Date(issue.createdAt),
title: issue.title,
closed: issue.closed,
closedDate: issue.closedAt == null ? undefined : new Date(issue.closedAt),
};
} catch (ex) {
Logger.error(ex, cc);
if (ex.code === 401) {
throw new AuthenticationError(ex);
}
throw ex;
}
}
}
interface GitHubIssueOrPullRequest {

+ 4
- 0
src/system/string.ts Переглянути файл

@ -38,6 +38,10 @@ export function escapeMarkdown(s: string, options: { quoted?: boolean } = {}): s
return s.replace(markdownQuotedRegex, '\t\n> ');
}
export function equalsIgnoreCase(a: string, b: string): boolean {
return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
}
export function escapeRegex(s: string) {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}

+ 7
- 3
src/views/contributorsView.ts Переглянути файл

@ -1,5 +1,5 @@
'use strict';
import { commands, ConfigurationChangeEvent, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { commands, ConfigurationChangeEvent, Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { configuration, ContributorsViewConfig, ViewFilesLayout } from '../configuration';
import { Container } from '../container';
import { Repository, RepositoryChange, RepositoryChangeEvent } from '../git/git';
@ -13,8 +13,9 @@ import {
unknownGitUri,
ViewNode,
} from './nodes';
import { debug, gate } from '../system';
import { debug, Functions, gate } from '../system';
import { ViewBase } from './viewBase';
import { onDidFetchAvatar } from '../avatars';
export class ContributorsRepositoryNode extends SubscribeableViewNode<ContributorsView> {
protected splatted = true;
@ -72,7 +73,10 @@ export class ContributorsRepositoryNode extends SubscribeableViewNode
@debug()
protected subscribe() {
return this.repo.onDidChange(this.onRepositoryChanged, this);
return Disposable.from(
this.repo.onDidChange(this.onRepositoryChanged, this),
onDidFetchAvatar(Functions.debounce(() => this.refresh(), 500)),
);
}
@debug({

+ 0
- 1
src/views/nodes/branchNode.ts Переглянути файл

@ -15,7 +15,6 @@ import {
GitBranchReference,
GitLog,
GitRemoteType,
GitRevision,
PullRequestState,
} from '../../git/git';
import { GitUri } from '../../git/gitUri';

+ 1
- 1
src/views/nodes/commitFileNode.ts Переглянути файл

@ -90,7 +90,7 @@ export class CommitFileNode extends ViewRefFileNode {
if (!this.commit.isUncommitted && !(this.view instanceof StashesView) && this.view.config.avatars) {
item.iconPath = this._options.unpublished
? new ThemeIcon('arrow-up')
: this.commit.getAvatarUri(Container.config.defaultGravatarsStyle);
: await this.commit.getAvatarUri(true, { fallback: Container.config.defaultGravatarsStyle });
}
}

+ 2
- 2
src/views/nodes/commitNode.ts Переглянути файл

@ -98,7 +98,7 @@ export class CommitNode extends ViewRefNode
return children;
}
getTreeItem(): TreeItem {
async getTreeItem(): Promise<TreeItem> {
const label = CommitFormatter.fromTemplate(this.view.config.commitFormat, this.commit, {
dateFormat: Container.config.defaultDateFormat,
getBranchAndTagTips: (sha: string) => this.getBranchAndTagTips?.(sha, true),
@ -121,7 +121,7 @@ export class CommitNode extends ViewRefNode
item.iconPath = this.unpublished
? new ThemeIcon('arrow-up')
: !(this.view instanceof StashesView) && this.view.config.avatars
? this.commit.getAvatarUri(Container.config.defaultGravatarsStyle)
? await this.commit.getAvatarUri(true, { fallback: Container.config.defaultGravatarsStyle })
: new ThemeIcon('git-commit');
item.tooltip = this.tooltip;

+ 4
- 2
src/views/nodes/contributorNode.ts Переглянути файл

@ -59,7 +59,7 @@ export class ContributorNode extends ViewNode
return children;
}
getTreeItem(): TreeItem {
async getTreeItem(): Promise<TreeItem> {
const presence = this._presenceMap?.get(this.contributor.email);
const item = new TreeItem(
@ -80,7 +80,9 @@ export class ContributorNode extends ViewNode
}\n${Strings.pluralize('commit', this.contributor.count)}`;
if (this.view.config.avatars) {
item.iconPath = this.contributor.getAvatarUri(Container.config.defaultGravatarsStyle);
item.iconPath = await this.contributor.getAvatarUri(true, {
fallback: Container.config.defaultGravatarsStyle,
});
}
return item;

+ 4
- 0
src/views/nodes/contributorsNode.ts Переглянути файл

@ -17,6 +17,8 @@ export class ContributorsNode extends ViewNode
return `${RepositoryNode.getId(repoPath)}${this.key}`;
}
protected splatted = true;
private _children: ViewNode[] | undefined;
constructor(
@ -47,6 +49,8 @@ export class ContributorsNode extends ViewNode
}
getTreeItem(): TreeItem {
this.splatted = false;
const item = new TreeItem('Contributors', TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ContextValues.Contributors;

+ 6
- 2
src/webviews/rebaseEditor.ts Переглянути файл

@ -155,7 +155,9 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
if (!authors.has(commit.author)) {
authors.set(commit.author, {
author: commit.author,
avatarUrl: commit.getAvatarUri(Container.config.defaultGravatarsStyle).toString(true),
avatarUrl: (
await commit.getAvatarUri(true, { fallback: Container.config.defaultGravatarsStyle })
).toString(true),
email: commit.email,
});
}
@ -180,7 +182,9 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
if (!authors.has(commit.author)) {
authors.set(commit.author, {
author: commit.author,
avatarUrl: commit.getAvatarUri(Container.config.defaultGravatarsStyle).toString(true),
avatarUrl: (
await commit.getAvatarUri(true, { fallback: Container.config.defaultGravatarsStyle })
).toString(true),
email: commit.email,
});
}

Завантаження…
Відмінити
Зберегти