@ -0,0 +1,4 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 22"> | |||
<path fill="#c5c5c5" d="M16 16.5c0 .44-.45 1-1 1H8a1 1 0 0 1-1-1H1c-.54 0-1-.56-1-1 0-2.63 3-4 3-4s.23-.4 0-1c-.84-.62-1.06-.59-1-3 .06-2.42 1.37-3 2.5-3s2.44.58 2.5 3c.06 2.41-.16 2.38-1 3-.23.6 0 1 0 1s1.55.71 2.42 2.09C9.2 12.87 10 12.5 10 12.5s.23-.4 0-1c-.84-.62-1.06-.59-1-3 .06-2.42 1.37-3 2.5-3s2.44.58 2.5 3c.05 2.41-.16 2.38-1 3-.23.59 0 1 0 1s3 1.37 3 4z"/> | |||
</svg> |
@ -0,0 +1,4 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 22"> | |||
<path fill="#424242" d="M16 16.5c0 .44-.45 1-1 1H8a1 1 0 0 1-1-1H1c-.54 0-1-.56-1-1 0-2.63 3-4 3-4s.23-.4 0-1c-.84-.62-1.06-.59-1-3 .06-2.42 1.37-3 2.5-3s2.44.58 2.5 3c.06 2.41-.16 2.38-1 3-.23.6 0 1 0 1s1.55.71 2.42 2.09C9.2 12.87 10 12.5 10 12.5s.23-.4 0-1c-.84-.62-1.06-.59-1-3 .06-2.42 1.37-3 2.5-3s2.44.58 2.5 3c.05 2.41-.16 2.38-1 3-.23.59 0 1 0 1s3 1.37 3 4z"/> | |||
</svg> |
@ -0,0 +1,17 @@ | |||
'use strict'; | |||
import { Uri } from 'vscode'; | |||
import { GravatarDefaultStyle } from '../../configuration'; | |||
import { getGravatarUri } from '../../gravatar'; | |||
export class GitContributor { | |||
constructor( | |||
public readonly repoPath: string, | |||
public readonly name: string, | |||
public readonly email: string, | |||
public readonly count: number | |||
) {} | |||
getGravatarUri(fallback: GravatarDefaultStyle, size: number = 16): Uri { | |||
return getGravatarUri(this.email, fallback, size); | |||
} | |||
} |
@ -0,0 +1,7 @@ | |||
'use strict'; | |||
import { GitContributor } from './contributor'; | |||
export interface GitShortLog { | |||
readonly repoPath: string; | |||
readonly contributors: GitContributor[]; | |||
} |
@ -0,0 +1,37 @@ | |||
'use strict'; | |||
import { GitContributor, GitShortLog } from '../git'; | |||
const shortlogRegex = /^(.*?)\t(.*?) <(.*?)>$/gm; | |||
export class GitShortLogParser { | |||
static parse(data: string, repoPath: string): GitShortLog | undefined { | |||
if (!data) return undefined; | |||
const contributors: GitContributor[] = []; | |||
let count; | |||
let name; | |||
let email; | |||
let match: RegExpExecArray | null = null; | |||
do { | |||
match = shortlogRegex.exec(data); | |||
if (match == null) break; | |||
[, count, name, email] = match; | |||
contributors.push( | |||
new GitContributor( | |||
repoPath, | |||
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 | |||
` ${name}`.substr(1), | |||
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 | |||
` ${email}`.substr(1), | |||
parseInt(count, 10) | |||
) | |||
); | |||
} while (match != null); | |||
if (!contributors.length) return undefined; | |||
return { repoPath: repoPath, contributors: contributors }; | |||
} | |||
} |
@ -0,0 +1,25 @@ | |||
'use strict'; | |||
import { Uri } from 'vscode'; | |||
import { GravatarDefaultStyle } from './config'; | |||
import { Strings } from './system'; | |||
const gravatarCache: Map<string, Uri> = new Map(); | |||
const missingGravatarHash = '00000000000000000000000000000000'; | |||
export function clearGravatarCache() { | |||
gravatarCache.clear(); | |||
} | |||
export function getGravatarUri(email: string | undefined, fallback: GravatarDefaultStyle, size: number = 16): Uri { | |||
const hash = | |||
email != null && email.length !== 0 ? Strings.md5(email.trim().toLowerCase(), 'hex') : missingGravatarHash; | |||
const key = `${hash}:${size}`; | |||
let gravatar = gravatarCache.get(key); | |||
if (gravatar !== undefined) return gravatar; | |||
gravatar = Uri.parse(`https://www.gravatar.com/avatar/${hash}.jpg?s=${size}&d=${fallback}`); | |||
gravatarCache.set(key, gravatar); | |||
return gravatar; | |||
} |
@ -0,0 +1,66 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitContributor, GitUri } from '../../git/gitService'; | |||
import { Iterables, Strings } from '../../system'; | |||
import { RepositoriesView } from '../repositoriesView'; | |||
import { PageableViewNode, ResourceType, ViewNode } from './viewNode'; | |||
import { Container } from '../../container'; | |||
import { MessageNode, ShowMoreNode } from './common'; | |||
import { getBranchesAndTagTipsFn, insertDateMarkers } from './helpers'; | |||
import { CommitNode } from './commitNode'; | |||
export class ContributorNode extends ViewNode<RepositoriesView> implements PageableViewNode { | |||
readonly supportsPaging: boolean = true; | |||
maxCount: number | undefined; | |||
constructor(uri: GitUri, view: RepositoriesView, parent: ViewNode, public readonly contributor: GitContributor) { | |||
super(uri, view, parent); | |||
} | |||
get id(): string { | |||
return `${this._instanceId}:gitlens:repository(${this.contributor.repoPath}):contributor(${ | |||
this.contributor.name | |||
}|${this.contributor.email}}`; | |||
} | |||
async getChildren(): Promise<ViewNode[]> { | |||
const log = await Container.git.getLog(this.uri.repoPath!, { | |||
maxCount: this.maxCount || this.view.config.defaultItemLimit, | |||
authors: [this.contributor.name] | |||
}); | |||
if (log === undefined) return [new MessageNode(this.view, this, 'No commits could be found.')]; | |||
const getBranchAndTagTips = await getBranchesAndTagTipsFn(this.uri.repoPath); | |||
const children = [ | |||
...insertDateMarkers( | |||
Iterables.map( | |||
log.commits.values(), | |||
c => new CommitNode(this.view, this, c, undefined, getBranchAndTagTips) | |||
), | |||
this | |||
) | |||
]; | |||
if (log.truncated) { | |||
children.push(new ShowMoreNode(this.view, this, 'Commits')); | |||
} | |||
return children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem(this.contributor.name, TreeItemCollapsibleState.Collapsed); | |||
item.id = this.id; | |||
item.contextValue = ResourceType.Contributor; | |||
item.description = this.contributor.email; | |||
item.tooltip = `${this.contributor.name} <${this.contributor.email}>\n${Strings.pluralize( | |||
'commit', | |||
this.contributor.count | |||
)}`; | |||
if (this.view.config.avatars) { | |||
item.iconPath = this.contributor.getGravatarUri(Container.config.defaultGravatarsStyle); | |||
} | |||
return item; | |||
} | |||
} |
@ -0,0 +1,41 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitUri, Repository } from '../../git/gitService'; | |||
import { RepositoriesView } from '../repositoriesView'; | |||
import { MessageNode } from './common'; | |||
import { ContributorNode } from './contributorNode'; | |||
import { ResourceType, ViewNode } from './viewNode'; | |||
import { Container } from '../../container'; | |||
export class ContributorsNode extends ViewNode<RepositoriesView> { | |||
constructor(uri: GitUri, view: RepositoriesView, parent: ViewNode, public readonly repo: Repository) { | |||
super(uri, view, parent); | |||
} | |||
get id(): string { | |||
return `${this._instanceId}:gitlens:repository(${this.repo.path}):contributors`; | |||
} | |||
async getChildren(): Promise<ViewNode[]> { | |||
const contributors = await this.repo.getContributors(); | |||
if (contributors.length === 0) return [new MessageNode(this.view, this, 'No contributors could be found.')]; | |||
contributors.sort((a, b) => b.count - a.count); | |||
const children = contributors.map(c => new ContributorNode(this.uri, this.view, this, c)); | |||
return children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem('Contributors', TreeItemCollapsibleState.Collapsed); | |||
item.id = this.id; | |||
item.contextValue = ResourceType.Contributors; | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath('images/dark/icon-people.svg'), | |||
light: Container.context.asAbsolutePath('images/light/icon-people.svg') | |||
}; | |||
return item; | |||
} | |||
} |