From d91c598ce0fc733a84419d8f16da19bad8a8e8d2 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 14 Dec 2017 01:27:32 -0500 Subject: [PATCH] Closes #230 - adds gravatar support to view --- CHANGELOG.md | 2 ++ README.md | 1 + package.json | 6 ++++++ src/configuration.ts | 2 ++ src/git/git.ts | 2 +- src/git/models/logCommit.ts | 28 +++++++++++++++++++++++++++- src/git/models/stashCommit.ts | 1 + src/git/parsers/logParser.ts | 6 ++++++ src/quickPicks/repoStatus.ts | 4 ++-- src/system/string.ts | 5 +++++ src/views/commitFileNode.ts | 25 +++++++++++++++++-------- src/views/commitNode.ts | 12 ++++++++---- src/views/fileHistoryNode.ts | 7 +++++-- src/views/statusFilesNode.ts | 8 ++++---- 14 files changed, 87 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ab1c80..ced8394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Adds a new `Active Repository` node to `Repository View` of the `GitLens` view -- closes [#224](https://github.com/eamodio/vscode-gitlens/issues/224) - Automatically updates to track the repository of the active editor - Only visible if there is more than 1 repository within the workspace +- Adds [Gravatar](https://en.gravatar.com/) support to the `GitLens` view +- Adds `gitlens.gitExplorer.gravatars` setting to specify whether or not to show gravatar images instead of commit (or status) icons in the `GitLens` view ### Fixed - Fixes [#228](https://github.com/eamodio/vscode-gitlens/issues/228) - Gutter blame spills over heatmap diff --git a/README.md b/README.md index 2ea93bd..33a99fc 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,7 @@ GitLens is highly customizable and provides many configuration settings to allow |`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` view" |`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` view
Available tokens
${id} - commit id
${author} - commit author
${message} - commit message
${ago} - relative commit date (e.g. 1 day ago)
${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
${authorAgo} - commit author, relative commit date
See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |`gitlens.gitExplorer.commitFileFormat`|Specifies the format of a committed file in the `GitLens` view
Available tokens
${directory} - directory name
${file} - file name
${filePath} - formatted file name and path
${path} - full file path +|`gitlens.gitExplorer.gravatars`|Specifies whether or not to show gravatar images instead of commit (or status) icons in the `GitLens` view |`gitlens.gitExplorer.stashFormat`|Specifies the format of stashed changes in the `GitLens` view
Available tokens
${id} - commit id
${author} - commit author
${message} - commit message
${ago} - relative commit date (e.g. 1 day ago)
${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
${authorAgo} - commit author, relative commit date
See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |`gitlens.gitExplorer.stashFileFormat`|Specifies the format of a stashed file in the `GitLens` view
Available tokens
${directory} - directory name
${file} - file name
${filePath} - formatted file name and path
${path} - full file path |`gitlens.gitExplorer.statusFileFormat`|Specifies the format of the status of a working or committed file in the `GitLens` view
Available tokens
${directory} - directory name
${file} - file name
${filePath} - formatted file name and path
${path} - full file path
${working} - optional indicator if the file is uncommitted diff --git a/package.json b/package.json index b163d38..b745fea 100644 --- a/package.json +++ b/package.json @@ -522,6 +522,12 @@ "description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` view\nOnly applies when displaying files as `auto`", "scope": "window" }, + "gitlens.gitExplorer.gravatars": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to show gravatar images instead of commit (or status) icons in the `GitLens` view", + "scope": "window" + }, "gitlens.gitExplorer.includeWorkingTree": { "type": "boolean", "default": true, diff --git a/src/configuration.ts b/src/configuration.ts index 2a70d4e..4775182 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -150,6 +150,7 @@ export interface IGitExplorerConfig { showTrackingBranch: boolean; commitFormat: string; commitFileFormat: string; + gravatars: boolean; stashFormat: string; stashFileFormat: string; statusFileFormat: string; @@ -387,6 +388,7 @@ const emptyConfig: IConfig = { showTrackingBranch: false, commitFormat: '', commitFileFormat: '', + gravatars: false, stashFormat: '', stashFileFormat: '', statusFileFormat: '' diff --git a/src/git/git.ts b/src/git/git.ts index da8e7c0..bc6daf3 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -24,7 +24,7 @@ export * from './remotes/provider'; let git: IGit; const defaultBlameParams = [`blame`, `--root`, `--incremental`]; -const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`]; +const defaultLogParams = [`log`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor %an%nauthor-email %ae%nauthor-date %at%nparents %P%nsummary %B%nfilename ?`]; const defaultStashParams = [`stash`, `list`, `--name-status`, `--full-history`, `-M`, `--format=%H -%nauthor-date %at%nreflog-selector %gd%nsummary %B%nfilename ?`]; const GitWarnings = [ diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index 58a8099..aabb3b4 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -1,10 +1,13 @@ 'use strict'; +import { Strings } from '../../system'; import { Uri } from 'vscode'; import { GitCommit, GitCommitType } from './commit'; import { Git } from '../git'; import { GitStatusFileStatus, IGitStatusFile } from './status'; import * as path from 'path'; +const gravatarCache: Map = new Map(); + export class GitLogCommit extends GitCommit { nextSha?: string; @@ -15,6 +18,7 @@ export class GitLogCommit extends GitCommit { repoPath: string, sha: string, author: string, + public readonly email: string | undefined, date: Date, message: string, fileName: string, @@ -39,6 +43,27 @@ export class GitLogCommit extends GitCommit { ); } + get gravatarUri(): Uri { + const key = this.email + ? this.email.trim().toLowerCase() + : ''; + + let gravatar = gravatarCache.get(key); + if (gravatar !== undefined) return gravatar; + + gravatar = Uri.parse(`https://www.gravatar.com/avatar/${this.email ? Strings.md5(this.email) : '00000000000000000000000000000000'}.jpg?s=22&d=retro`); + + // HACK: Monkey patch Uri.toString to avoid the unwanted query string encoding + const originalToStringFn = gravatar.toString; + gravatar.toString = function(skipEncoding?: boolean | undefined) { + return originalToStringFn.call(gravatar, true); + }; + + gravatarCache.set(key, gravatar); + + return gravatar; + } + get isMerge() { return this.parentShas && this.parentShas.length > 1; } @@ -111,12 +136,13 @@ export class GitLogCommit extends GitCommit { }); } - with(changes: { type?: GitCommitType, sha?: string | null, fileName?: string, author?: string, date?: Date, message?: string, originalFileName?: string | null, previousFileName?: string | null, previousSha?: string | null, status?: GitStatusFileStatus, fileStatuses?: IGitStatusFile[] | null }): GitLogCommit { + with(changes: { type?: GitCommitType, sha?: string | null, fileName?: string, author?: string, email?: string, date?: Date, message?: string, originalFileName?: string | null, previousFileName?: string | null, previousSha?: string | null, status?: GitStatusFileStatus, fileStatuses?: IGitStatusFile[] | null }): GitLogCommit { return new GitLogCommit( changes.type || this.type, this.repoPath, this.getChangedValue(changes.sha, this.sha)!, changes.author || this.author, + changes.email || this.email, changes.date || this.date, changes.message || this.message, changes.fileName || this.fileName, diff --git a/src/git/models/stashCommit.ts b/src/git/models/stashCommit.ts index b64390e..1f623d1 100644 --- a/src/git/models/stashCommit.ts +++ b/src/git/models/stashCommit.ts @@ -24,6 +24,7 @@ export class GitStashCommit extends GitLogCommit { repoPath, sha, 'You', + undefined, date, message, fileName, diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 8ff1ae8..d927e7b 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -9,6 +9,7 @@ interface LogEntry { sha: string; author: string; + authorEmail?: string; authorDate?: string; parentShas?: string[]; @@ -83,6 +84,10 @@ export class GitLogParser { : lineParts.slice(1).join(' ').trim(); break; + case 'author-email': + entry.authorEmail = lineParts.slice(1).join(' ').trim(); + break; + case 'author-date': entry.authorDate = lineParts[1]; break; @@ -241,6 +246,7 @@ export class GitLogParser { repoPath!, entry.sha, entry.author, + entry.authorEmail, new Date(entry.authorDate! as any * 1000), entry.summary!, relativeFileName, diff --git a/src/quickPicks/repoStatus.ts b/src/quickPicks/repoStatus.ts index de429cc..05308ed 100644 --- a/src/quickPicks/repoStatus.ts +++ b/src/quickPicks/repoStatus.ts @@ -28,10 +28,10 @@ export class OpenStatusFileCommandQuickPickItem extends OpenFileCommandQuickPick this.status = status; if (status.indexStatus !== undefined) { - this.commit = new GitLogCommit(GitCommitType.File, status.repoPath, GitService.stagedUncommittedSha, 'You', new Date(), '', status.fileName, [status], status.status, status.originalFileName, 'HEAD', status.fileName); + this.commit = new GitLogCommit(GitCommitType.File, status.repoPath, GitService.stagedUncommittedSha, 'You', undefined, new Date(), '', status.fileName, [status], status.status, status.originalFileName, 'HEAD', status.fileName); } else { - this.commit = new GitLogCommit(GitCommitType.File, status.repoPath, GitService.uncommittedSha, 'You', new Date(), '', status.fileName, [status], status.status, status.originalFileName, realIndexStatus !== undefined ? GitService.stagedUncommittedSha : 'HEAD', status.fileName); + this.commit = new GitLogCommit(GitCommitType.File, status.repoPath, GitService.uncommittedSha, 'You', undefined, new Date(), '', status.fileName, [status], status.status, status.originalFileName, realIndexStatus !== undefined ? GitService.stagedUncommittedSha : 'HEAD', status.fileName); } } diff --git a/src/system/string.ts b/src/system/string.ts index c9d9bae..85ea261 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -1,5 +1,6 @@ 'use strict'; const _escapeRegExp = require('lodash.escaperegexp'); +import * as crypto from 'crypto'; export namespace Strings { export function escapeRegExp(s: string): string { @@ -57,6 +58,10 @@ export namespace Strings { } } + export function md5(s: string): string { + return crypto.createHash('md5').update(s).digest('hex'); + } + export function pad(s: string, before: number = 0, after: number = 0, padding: string = `\u00a0`) { if (before === 0 && after === 0) return s; diff --git a/src/views/commitFileNode.ts b/src/views/commitFileNode.ts index 2e442db..05e1637 100644 --- a/src/views/commitFileNode.ts +++ b/src/views/commitFileNode.ts @@ -11,6 +11,7 @@ export enum CommitFileNodeDisplayAs { CommitIcon = 1 << 1, FileLabel = 1 << 2, StatusIcon = 1 << 3, + Gravatar = 1 << 4, Commit = CommitLabel | CommitIcon, File = FileLabel | StatusIcon @@ -54,14 +55,22 @@ export class CommitFileNode extends ExplorerNode { const item = new TreeItem(this.label, TreeItemCollapsibleState.None); item.contextValue = this.resourceType; - const icon = (this.displayAs & CommitFileNodeDisplayAs.CommitIcon) - ? 'icon-commit.svg' - : getGitStatusIcon(this.status.status); - - item.iconPath = { - dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)), - light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon)) - }; + if ((this.displayAs & CommitFileNodeDisplayAs.CommitIcon) === CommitFileNodeDisplayAs.CommitIcon) { + item.iconPath = { + dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', 'icon-commit.svg')), + light: this.explorer.context.asAbsolutePath(path.join('images', 'light', 'icon-commit.svg')) + }; + } + else if ((this.displayAs & CommitFileNodeDisplayAs.StatusIcon) === CommitFileNodeDisplayAs.StatusIcon) { + const icon = getGitStatusIcon(this.status.status); + item.iconPath = { + dark: this.explorer.context.asAbsolutePath(path.join('images', 'dark', icon)), + light: this.explorer.context.asAbsolutePath(path.join('images', 'light', icon)) + }; + } + else if ((this.displayAs & CommitFileNodeDisplayAs.Gravatar) === CommitFileNodeDisplayAs.Gravatar) { + item.iconPath = this.commit.gravatarUri; + } item.command = this.getCommand(); diff --git a/src/views/commitNode.ts b/src/views/commitNode.ts index b704518..1aaa50f 100644 --- a/src/views/commitNode.ts +++ b/src/views/commitNode.ts @@ -59,10 +59,14 @@ export class CommitNode extends ExplorerNode { ? ResourceType.CommitOnCurrentBranch : ResourceType.Commit; - item.iconPath = { - dark: this.explorer.context.asAbsolutePath('images/dark/icon-commit.svg'), - light: this.explorer.context.asAbsolutePath('images/light/icon-commit.svg') - }; + if (this.explorer.config.gravatars) { + item.iconPath = this.commit.gravatarUri; + } else { + item.iconPath = { + dark: this.explorer.context.asAbsolutePath('images/dark/icon-commit.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-commit.svg') + }; + } return item; } diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts index 2ea5fc9..9c271bf 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -22,6 +22,8 @@ export class FileHistoryNode extends ExplorerNode { const children: ExplorerNode[] = []; + const displayAs = CommitFileNodeDisplayAs.CommitLabel | (this.explorer.config.gravatars ? CommitFileNodeDisplayAs.Gravatar : CommitFileNodeDisplayAs.StatusIcon); + const status = await this.explorer.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath); if (status !== undefined && (status.indexStatus !== undefined || status.workTreeStatus !== undefined)) { let sha; @@ -45,6 +47,7 @@ export class FileHistoryNode extends ExplorerNode { this.uri.repoPath!, sha, 'You', + undefined, new Date(), '', status.fileName, @@ -53,12 +56,12 @@ export class FileHistoryNode extends ExplorerNode { status.originalFileName, previousSha, status.originalFileName || status.fileName); - children.push(new CommitFileNode(status, commit, this.explorer, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon)); + children.push(new CommitFileNode(status, commit, this.explorer, displayAs)); } const log = await this.explorer.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); if (log !== undefined) { - children.push(...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))); + children.push(...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, displayAs))); } if (children.length === 0) return [new MessageNode('No file history')]; diff --git a/src/views/statusFilesNode.ts b/src/views/statusFilesNode.ts index e658cf3..66443bc 100644 --- a/src/views/statusFilesNode.ts +++ b/src/views/statusFilesNode.ts @@ -53,12 +53,12 @@ export class StatusFilesNode extends ExplorerNode { { ...s, status: s.status, - commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.uncommittedSha, 'You', new Date(), '', s.fileName, [s], s.status, s.originalFileName, GitService.stagedUncommittedSha, s.fileName) + commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.uncommittedSha, 'You', undefined, new Date(), '', s.fileName, [s], s.status, s.originalFileName, GitService.stagedUncommittedSha, s.fileName) } as IGitStatusFileWithCommit, { ...s, status: s.status, - commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.stagedUncommittedSha, 'You', older, '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) + commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.stagedUncommittedSha, 'You', undefined, older, '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit ]; } @@ -67,7 +67,7 @@ export class StatusFilesNode extends ExplorerNode { { ...s, status: s.status, - commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.stagedUncommittedSha, 'You', new Date(), '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) + commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.stagedUncommittedSha, 'You', undefined, new Date(), '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit ]; } @@ -76,7 +76,7 @@ export class StatusFilesNode extends ExplorerNode { { ...s, status: s.status, - commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.uncommittedSha, 'You', new Date(), '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) + commit: new GitLogCommit(GitCommitType.File, repoPath, GitService.uncommittedSha, 'You', undefined, new Date(), '', s.fileName, [s], s.status, s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit ]; }