Browse Source

Adds contributors node to repos

main
Eric Amodio 5 years ago
parent
commit
3c4bb8c900
23 changed files with 380 additions and 33 deletions
  1. +30
    -0
      CHANGELOG.md
  2. +14
    -1
      README.md
  3. +4
    -0
      images/dark/icon-people.svg
  4. +4
    -0
      images/light/icon-people.svg
  5. +37
    -0
      package.json
  6. +2
    -1
      src/container.ts
  7. +4
    -0
      src/git/git.ts
  8. +29
    -8
      src/git/gitService.ts
  9. +4
    -21
      src/git/models/commit.ts
  10. +17
    -0
      src/git/models/contributor.ts
  11. +2
    -0
      src/git/models/models.ts
  12. +5
    -1
      src/git/models/repository.ts
  13. +7
    -0
      src/git/models/shortlog.ts
  14. +1
    -0
      src/git/parsers/parsers.ts
  15. +37
    -0
      src/git/parsers/shortlogParser.ts
  16. +25
    -0
      src/gravatar.ts
  17. +2
    -0
      src/views/nodes.ts
  18. +66
    -0
      src/views/nodes/contributorNode.ts
  19. +41
    -0
      src/views/nodes/contributorsNode.ts
  20. +2
    -0
      src/views/nodes/repositoryNode.ts
  21. +2
    -0
      src/views/nodes/viewNode.ts
  22. +44
    -1
      src/views/viewCommands.ts
  23. +1
    -0
      src/vsls/host.ts

+ 30
- 0
CHANGELOG.md View File

@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Adds a new _Contributors_ node to each repository in the _Repositories_ view
- **Contributors** — lists the contributors in the repository, sorted by contributed commits
- Provides the avatar (if enabled), name, and email address of each contributor
- An inline toolbar provides quick access to the _Copy to Clipboard_ command
- A context menu provides access to the _Copy to Clipboard_, _Add as Co-author_, and _Refresh_ commands
- Each contributor expands to list the repository's revision (commit) history filtered by the contributor
- An inline toolbar provides quick access to the _Compare with HEAD_ (`alt-click` for _Compare with Working Tree_), _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open Commit on Remote_ (if available) commands
- A context menu provides access to more common revision (commit) commands
- Each revision (commit) expands to list its set of changed files, complete with status indicators for adds, changes, renames, and deletes
- An inline toolbar provides quick access to the _Open File_, _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open File on Remote_ (if available) commands
- A context menu provides access to more common file revision commands
- Adds a _Collapse All_ command to the _Repositories_ view — closes [#688](https://github.com/eamodio/vscode-gitlens/issues/688)
- Adds version links to CHANGELOG — closes [#617](https://github.com/eamodio/vscode-gitlens/issues/617) thanks to [PR #600](https://github.com/eamodio/vscode-gitlens/pull/660) by John Gee ([@shadowspawn](https://github.com/shadowspawn))
### Changed
- Updates the invite link to the [VS Code Development Community Slack](https://vscode-slack.amod.io)
- Improves the behavior of the _Open Changes with Next Revision_ (`gitlens.diffWithNext`) command when in the diff editor
- Improves the behavior of the _Open Changes with Previous Revision_ (`gitlens.diffWithPrevious`) command when in the diff editor
- Improves the behavior of the _Open Changes with Working File_ (`gitlens.diffWithWorking`) command when in the diff editor
### Fixed
- Fixes [#683](https://github.com/eamodio/vscode-gitlens/issues/683) - log.showSignature leads to stray files being displayed
- Fixes the behavior of the _Open Line Changes with Previous Revision_ (`gitlens.diffLineWithPrevious`) command to follow the line history much better
## [9.5.1] - 2019-02-13 ## [9.5.1] - 2019-02-13
### Added ### Added

+ 14
- 1
README.md View File

@ -304,12 +304,24 @@ The repositories view provides the following features,
- An inline toolbar provides quick access to the _Open File_, _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open File on Remote_ (if available) commands - An inline toolbar provides quick access to the _Open File_, _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open File on Remote_ (if available) commands
- A context menu provides access to more common file revision commands - A context menu provides access to more common file revision commands
- **Contributors** — lists the contributors in the repository, sorted by contributed commits
- Provides the avatar (if enabled), name, and email address of each contributor
- An inline toolbar provides quick access to the _Copy to Clipboard_ command
- A context menu provides access to the _Copy to Clipboard_, _Add as Co-author_, and _Refresh_ commands
- Each contributor expands to list the repository's revision (commit) history filtered by the contributor
- An inline toolbar provides quick access to the _Compare with HEAD_ (`alt-click` for _Compare with Working Tree_), _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open Commit on Remote_ (if available) commands
- A context menu provides access to more common revision (commit) commands
- Each revision (commit) expands to list its set of changed files, complete with status indicators for adds, changes, renames, and deletes
- An inline toolbar provides quick access to the _Open File_, _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open File on Remote_ (if available) commands
- A context menu provides access to more common file revision commands
- **Remotes** — lists the remotes in the repository - **Remotes** — lists the remotes in the repository
- Provides the name of each remote, an indicator of the direction of the remote (fetch, push, both), remote service (if applicable), and repository path - Provides the name of each remote, an indicator of the direction of the remote (fetch, push, both), remote service (if applicable), and repository path
- An inline toolbar provides quick access to the _Fetch_, and _Open Repository on Remote_ (if available) commands - An inline toolbar provides quick access to the _Fetch_, and _Open Repository on Remote_ (if available) commands
- A context menu provides access to more common repository and remote commands - A context menu provides access to more common repository and remote commands
- Each remote expands list its remote branches
- Each remote expands to list its remote branches
- See the **Branches** above for additional details - See the **Branches** above for additional details
- **Stashes** — lists the stashed changes in the repository - **Stashes** — lists the stashed changes in the repository
@ -888,6 +900,7 @@ A big thanks to the people that have contributed to this project:
- Matt Cooper ([@vtbassmatt](https://github.com/vtbassmatt)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=vtbassmatt) - Matt Cooper ([@vtbassmatt](https://github.com/vtbassmatt)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=vtbassmatt)
- Segev Finer ([@segevfiner](https://github.com/segevfiner)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=segevfiner) - Segev Finer ([@segevfiner](https://github.com/segevfiner)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=segevfiner)
- Cory Forsyth ([@bantic](https://github.com/bantic)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=bantic) - Cory Forsyth ([@bantic](https://github.com/bantic)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=bantic)
- John Gee ([@shadowspawn](https://github.com/shadowspawn)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=shadowspawn)
- Geoffrey ([@g3offrey](https://github.com/g3offrey)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=g3offrey) - Geoffrey ([@g3offrey](https://github.com/g3offrey)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=g3offrey)
- Yukai Huang ([@Yukaii](https://github.com/Yukaii)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=Yukaii) - Yukai Huang ([@Yukaii](https://github.com/Yukaii)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=Yukaii)
- Roy Ivy III ([@rivy](https://github.com/rivy)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rivy) - Roy Ivy III ([@rivy](https://github.com/rivy)) — [contributions](https://github.com/eamodio/vscode-gitlens/commits?author=rivy)

+ 4
- 0
images/dark/icon-people.svg View File

@ -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>

+ 4
- 0
images/light/icon-people.svg View File

@ -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>

+ 37
- 0
package.json View File

@ -2477,6 +2477,20 @@
} }
}, },
{ {
"command": "gitlens.views.contributor.addCoauthoredBy",
"title": "Add as Co-author",
"category": "GitLens"
},
{
"command": "gitlens.views.contributor.copyToClipboard",
"title": "Copy to Clipboard",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-clipboard.svg",
"light": "images/light/icon-clipboard.svg"
}
},
{
"command": "gitlens.views.terminalCheckoutBranch", "command": "gitlens.views.terminalCheckoutBranch",
"title": "Checkout Branch (via Terminal)", "title": "Checkout Branch (via Terminal)",
"category": "GitLens" "category": "GitLens"
@ -3256,6 +3270,14 @@
"when": "false" "when": "false"
}, },
{ {
"command": "gitlens.views.contributor.addCoauthoredBy",
"when": "false"
},
{
"command": "gitlens.views.contributor.copyToClipboard",
"when": "false"
},
{
"command": "gitlens.views.terminalCheckoutBranch", "command": "gitlens.views.terminalCheckoutBranch",
"when": "false" "when": "false"
}, },
@ -4105,6 +4127,21 @@
"group": "8_gitlens_@1" "group": "8_gitlens_@1"
}, },
{ {
"command": "gitlens.views.contributor.copyToClipboard",
"when": "viewItem =~ /gitlens:contributor\\b/",
"group": "inline@98"
},
{
"command": "gitlens.views.contributor.copyToClipboard",
"when": "viewItem =~ /gitlens:contributor\\b/",
"group": "1_gitlens@1"
},
{
"command": "gitlens.views.contributor.addCoauthoredBy",
"when": "viewItem =~ /gitlens:contributor\\b/",
"group": "2_gitlens@1"
},
{
"command": "gitlens.openCommitInRemote", "command": "gitlens.openCommitInRemote",
"when": "viewItem =~ /gitlens:commit\\b/ && gitlens:hasRemotes", "when": "viewItem =~ /gitlens:commit\\b/ && gitlens:hasRemotes",
"group": "inline@98" "group": "inline@98"

+ 2
- 1
src/container.ts View File

@ -6,7 +6,8 @@ import { GitCodeLensController } from './codelens/codeLensController';
import { Commands, ToggleFileBlameCommandArgs } from './commands'; import { Commands, ToggleFileBlameCommandArgs } from './commands';
import { AnnotationsToggleMode, Config, configuration, ConfigurationWillChangeEvent } from './configuration'; import { AnnotationsToggleMode, Config, configuration, ConfigurationWillChangeEvent } from './configuration';
import { GitFileSystemProvider } from './git/fsProvider'; import { GitFileSystemProvider } from './git/fsProvider';
import { clearGravatarCache, GitService } from './git/gitService';
import { GitService } from './git/gitService';
import { clearGravatarCache } from './gravatar';
import { LineHoverController } from './hovers/lineHoverController'; import { LineHoverController } from './hovers/lineHoverController';
import { Keyboard } from './keyboard'; import { Keyboard } from './keyboard';
import { Logger, TraceLevel } from './logger'; import { Logger, TraceLevel } from './logger';

+ 4
- 0
src/git/git.ts View File

@ -811,6 +811,10 @@ export class Git {
return data.length === 0 ? undefined : data.trim(); return data.length === 0 ? undefined : data.trim();
} }
static shortlog(repoPath: string) {
return git<string>({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges');
}
static async show<TOut extends string | Buffer>( static async show<TOut extends string | Buffer>(
repoPath: string | undefined, repoPath: string | undefined,
fileName: string, fileName: string,

+ 29
- 8
src/git/gitService.ts View File

@ -6,6 +6,7 @@ import {
Disposable, Disposable,
Event, Event,
EventEmitter, EventEmitter,
Extension,
extensions, extensions,
ProgressLocation, ProgressLocation,
Range, Range,
@ -18,7 +19,7 @@ import {
WorkspaceFoldersChangeEvent WorkspaceFoldersChangeEvent
} from 'vscode'; } from 'vscode';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { GitExtension } from '../@types/git';
import { API as BuiltInGitApi, GitExtension } from '../@types/git';
import { configuration, RemotesConfig } from '../configuration'; import { configuration, RemotesConfig } from '../configuration';
import { CommandContext, DocumentSchemes, GlyphChars, setCommandContext } from '../constants'; import { CommandContext, DocumentSchemes, GlyphChars, setCommandContext } from '../constants';
import { Container } from '../container'; import { Container } from '../container';
@ -40,6 +41,7 @@ import {
GitBranchParser, GitBranchParser,
GitCommit, GitCommit,
GitCommitType, GitCommitType,
GitContributor,
GitDiff, GitDiff,
GitDiffChunkLine, GitDiffChunkLine,
GitDiffParser, GitDiffParser,
@ -64,6 +66,7 @@ import {
} from './git'; } from './git';
import { GitUri } from './gitUri'; import { GitUri } from './gitUri';
import { RemoteProviderFactory, RemoteProviders } from './remotes/factory'; import { RemoteProviderFactory, RemoteProviders } from './remotes/factory';
import { GitShortLogParser } from './parsers/parsers';
export * from './gitUri'; export * from './gitUri';
export * from './models/models'; export * from './models/models';
@ -1117,6 +1120,15 @@ export class GitService implements Disposable {
} }
@log() @log()
async getContributors(repoPath: string): Promise<GitContributor[]> {
if (repoPath === undefined) return [];
const data = await Git.shortlog(repoPath);
const shortlog = GitShortLogParser.parse(data, repoPath);
return shortlog === undefined ? [] : shortlog.contributors;
}
@log()
async getCurrentUser(repoPath: string) { async getCurrentUser(repoPath: string) {
let user = this._userMapCache.get(repoPath); let user = this._userMapCache.get(repoPath);
if (user != null) return user; if (user != null) return user;
@ -2218,18 +2230,27 @@ export class GitService implements Disposable {
static async initialize(): Promise<void> { static async initialize(): Promise<void> {
// Try to use the same git as the built-in vscode git extension // Try to use the same git as the built-in vscode git extension
let gitPath; let gitPath;
const gitApi = await GitService.getBuiltInGitApi();
if (gitApi !== undefined) {
gitPath = gitApi.git.path;
}
await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get<string>('path'));
}
@log()
static async getBuiltInGitApi(): Promise<BuiltInGitApi | undefined> {
try { try {
const gitExtension = extensions.getExtension('vscode.git');
if (gitExtension !== undefined) {
const gitApi = ((gitExtension.isActive
? gitExtension.exports
: await gitExtension.activate()) as GitExtension).getAPI(1);
gitPath = gitApi.git.path;
const extension = extensions.getExtension('vscode.git') as Extension<GitExtension>;
if (extension !== undefined) {
const gitExtension = extension.isActive ? extension.exports : await extension.activate();
return gitExtension.getAPI(1);
} }
} }
catch {} catch {}
await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get<string>('path'));
return undefined;
} }
static getGitPath(): string { static getGitPath(): string {

+ 4
- 21
src/git/models/commit.ts View File

@ -2,17 +2,11 @@
import { Uri } from 'vscode'; import { Uri } from 'vscode';
import { configuration, DateStyle, GravatarDefaultStyle } from '../../configuration'; import { configuration, DateStyle, GravatarDefaultStyle } from '../../configuration';
import { Container } from '../../container'; import { Container } from '../../container';
import { Dates, Strings } from '../../system';
import { Dates } from '../../system';
import { CommitFormatter } from '../formatters/formatters'; import { CommitFormatter } from '../formatters/formatters';
import { Git } from '../git'; import { Git } from '../git';
import { GitUri } from '../gitUri'; import { GitUri } from '../gitUri';
const gravatarCache: Map<string, Uri> = new Map();
const missingGravatarHash = '00000000000000000000000000000000';
export function clearGravatarCache() {
gravatarCache.clear();
}
import { getGravatarUri } from '../../gravatar';
export interface GitAuthor { export interface GitAuthor {
name: string; name: string;
@ -180,22 +174,11 @@ export abstract class GitCommit {
} }
getGravatarUri(fallback: GravatarDefaultStyle, size: number = 16): Uri { getGravatarUri(fallback: GravatarDefaultStyle, size: number = 16): Uri {
const hash =
this.email != null && this.email.length !== 0
? Strings.md5(this.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;
return getGravatarUri(this.email, fallback, size);
} }
getShortMessage() { getShortMessage() {
// eslint-disable-next-line no-template-curly-in-string
return CommitFormatter.fromTemplate('${message}', this, { truncateMessageAtNewLine: true }); return CommitFormatter.fromTemplate('${message}', this, { truncateMessageAtNewLine: true });
} }

+ 17
- 0
src/git/models/contributor.ts View File

@ -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);
}
}

+ 2
- 0
src/git/models/models.ts View File

@ -4,12 +4,14 @@ export * from './blame';
export * from './blameCommit'; export * from './blameCommit';
export * from './branch'; export * from './branch';
export * from './commit'; export * from './commit';
export * from './contributor';
export * from './diff'; export * from './diff';
export * from './file'; export * from './file';
export * from './log'; export * from './log';
export * from './logCommit'; export * from './logCommit';
export * from './remote'; export * from './remote';
export * from './repository'; export * from './repository';
export * from './shortlog';
export * from './stash'; export * from './stash';
export * from './stashCommit'; export * from './stashCommit';
export * from './status'; export * from './status';

+ 5
- 1
src/git/models/repository.ts View File

@ -18,7 +18,7 @@ import { configuration, RemotesConfig } from '../../configuration';
import { StarredRepositories, WorkspaceState } from '../../constants'; import { StarredRepositories, WorkspaceState } from '../../constants';
import { Container } from '../../container'; import { Container } from '../../container';
import { Functions, gate, log } from '../../system'; import { Functions, gate, log } from '../../system';
import { GitBranch, GitDiffShortStat, GitRemote, GitStash, GitStatus, GitTag } from '../git';
import { GitBranch, GitContributor, GitDiffShortStat, GitRemote, GitStash, GitStatus, GitTag } from '../git';
import { GitUri } from '../gitUri'; import { GitUri } from '../gitUri';
import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory'; import { RemoteProviderFactory, RemoteProviders } from '../remotes/factory';
@ -272,6 +272,10 @@ export class Repository implements Disposable {
return Container.git.getChangedFilesCount(this.path, sha); return Container.git.getChangedFilesCount(this.path, sha);
} }
getContributors(): Promise<GitContributor[]> {
return Container.git.getContributors(this.path);
}
async getLastFetched(): Promise<number> { async getLastFetched(): Promise<number> {
const hasRemotes = await this.hasRemotes(); const hasRemotes = await this.hasRemotes();
if (!hasRemotes || Container.vsls.isMaybeGuest) return 0; if (!hasRemotes || Container.vsls.isMaybeGuest) return 0;

+ 7
- 0
src/git/models/shortlog.ts View File

@ -0,0 +1,7 @@
'use strict';
import { GitContributor } from './contributor';
export interface GitShortLog {
readonly repoPath: string;
readonly contributors: GitContributor[];
}

+ 1
- 0
src/git/parsers/parsers.ts View File

@ -5,6 +5,7 @@ export * from './branchParser';
export * from './diffParser'; export * from './diffParser';
export * from './logParser'; export * from './logParser';
export * from './remoteParser'; export * from './remoteParser';
export * from './shortlogParser';
export * from './stashParser'; export * from './stashParser';
export * from './statusParser'; export * from './statusParser';
export * from './tagParser'; export * from './tagParser';

+ 37
- 0
src/git/parsers/shortlogParser.ts View File

@ -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 };
}
}

+ 25
- 0
src/gravatar.ts View File

@ -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;
}

+ 2
- 0
src/views/nodes.ts View File

@ -6,6 +6,8 @@ export * from './nodes/branchNode';
export * from './nodes/branchTrackingStatusNode'; export * from './nodes/branchTrackingStatusNode';
export * from './nodes/commitFileNode'; export * from './nodes/commitFileNode';
export * from './nodes/commitNode'; export * from './nodes/commitNode';
export * from './nodes/contributorNode';
export * from './nodes/contributorsNode';
export * from './nodes/fileHistoryNode'; export * from './nodes/fileHistoryNode';
export * from './nodes/fileHistoryTrackerNode'; export * from './nodes/fileHistoryTrackerNode';
export * from './nodes/folderNode'; export * from './nodes/folderNode';

+ 66
- 0
src/views/nodes/contributorNode.ts View File

@ -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;
}
}

+ 41
- 0
src/views/nodes/contributorsNode.ts View File

@ -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;
}
}

+ 2
- 0
src/views/nodes/repositoryNode.ts View File

@ -22,6 +22,7 @@ import { StashesNode } from './stashesNode';
import { StatusFilesNode } from './statusFilesNode'; import { StatusFilesNode } from './statusFilesNode';
import { TagsNode } from './tagsNode'; import { TagsNode } from './tagsNode';
import { ResourceType, SubscribeableViewNode, ViewNode } from './viewNode'; import { ResourceType, SubscribeableViewNode, ViewNode } from './viewNode';
import { ContributorsNode } from './contributorsNode';
const hasTimeRegex = /[hHm]/; const hasTimeRegex = /[hHm]/;
@ -78,6 +79,7 @@ export class RepositoryNode extends SubscribeableViewNode {
children.push( children.push(
new BranchesNode(this.uri, this.view, this, this.repo), new BranchesNode(this.uri, this.view, this, this.repo),
new ContributorsNode(this.uri, this.view, this, this.repo),
new RemotesNode(this.uri, this.view, this, this.repo), new RemotesNode(this.uri, this.view, this, this.repo),
new StashesNode(this.uri, this.view, this, this.repo), new StashesNode(this.uri, this.view, this, this.repo),
new TagsNode(this.uri, this.view, this, this.repo) new TagsNode(this.uri, this.view, this, this.repo)

+ 2
- 0
src/views/nodes/viewNode.ts View File

@ -19,6 +19,8 @@ export enum ResourceType {
ComparePicker = 'gitlens:compare:picker', ComparePicker = 'gitlens:compare:picker',
ComparePickerWithRef = 'gitlens:compare:picker:ref', ComparePickerWithRef = 'gitlens:compare:picker:ref',
CompareResults = 'gitlens:compare:results', CompareResults = 'gitlens:compare:results',
Contributor = 'gitlens:contributor',
Contributors = 'gitlens:contributors',
File = 'gitlens:file', File = 'gitlens:file',
FileHistory = 'gitlens:history:file', FileHistory = 'gitlens:history:file',
Folder = 'gitlens:folder', Folder = 'gitlens:folder',

+ 44
- 1
src/views/viewCommands.ts View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import * as paths from 'path'; import * as paths from 'path';
import { commands, Disposable, Terminal, TextDocumentShowOptions, Uri, window } from 'vscode';
import { commands, Disposable, env, Terminal, TextDocumentShowOptions, Uri, window } from 'vscode';
import { import {
Commands, Commands,
DiffWithCommandArgs, DiffWithCommandArgs,
@ -34,6 +34,8 @@ import {
ViewRefNode, ViewRefNode,
viewSupportsNodeDismissal viewSupportsNodeDismissal
} from './nodes'; } from './nodes';
import { ContributorNode } from './nodes/contributorNode';
import { Strings } from '../system/string';
export interface RefreshNodeCommandArgs { export interface RefreshNodeCommandArgs {
maxCount?: number; maxCount?: number;
@ -83,6 +85,9 @@ export class ViewCommands implements Disposable {
commands.registerCommand('gitlens.views.exploreRepoRevision', this.exploreRepoRevision, this); commands.registerCommand('gitlens.views.exploreRepoRevision', this.exploreRepoRevision, this);
commands.registerCommand('gitlens.views.contributor.addCoauthoredBy', this.contributorAddCoauthoredBy, this);
commands.registerCommand('gitlens.views.contributor.copyToClipboard', this.contributorCopyToClipboard, this);
commands.registerCommand('gitlens.views.openChanges', this.openChanges, this); commands.registerCommand('gitlens.views.openChanges', this.openChanges, this);
commands.registerCommand('gitlens.views.openChangesWithWorking', this.openChangesWithWorking, this); commands.registerCommand('gitlens.views.openChangesWithWorking', this.openChangesWithWorking, this);
commands.registerCommand('gitlens.views.openFile', this.openFile, this); commands.registerCommand('gitlens.views.openFile', this.openFile, this);
@ -139,6 +144,44 @@ export class ViewCommands implements Disposable {
this._disposable && this._disposable.dispose(); this._disposable && this._disposable.dispose();
} }
private async contributorAddCoauthoredBy(node: ContributorNode) {
if (!(node instanceof ContributorNode)) return;
const gitApi = await GitService.getBuiltInGitApi();
if (gitApi === undefined) return;
const repo = gitApi.repositories.find(
r => Strings.normalizePath(r.rootUri.fsPath) === node.contributor.repoPath
);
if (repo === undefined) return;
const coauthor = `${node.contributor.name}${node.contributor.email ? ` <${node.contributor.email}>` : ''}`;
const message = repo.inputBox.value;
if (message.includes(coauthor)) return;
let newlines;
if (message.includes('Co-authored-by: ')) {
newlines = '\n';
}
else if (message.length !== 0 && message[message.length - 1] === '\n') {
newlines = '\n\n';
}
else {
newlines = '\n\n\n';
}
repo.inputBox.value = `${message}${newlines}Co-authored-by: ${coauthor}`;
}
private async contributorCopyToClipboard(node: ContributorNode) {
if (!(node instanceof ContributorNode)) return;
await env.clipboard.writeText(
`${node.contributor.name}${node.contributor.email ? ` <${node.contributor.email}>` : ''}`
);
}
private fetch(node: RemoteNode | RepositoryNode) { private fetch(node: RemoteNode | RepositoryNode) {
if (node instanceof RemoteNode) return node.fetch(); if (node instanceof RemoteNode) return node.fetch();
if (node instanceof RepositoryNode) return node.fetch(); if (node instanceof RepositoryNode) return node.fetch();

+ 1
- 0
src/vsls/host.ts View File

@ -34,6 +34,7 @@ const gitWhitelist = new Map boolean>([
['merge-base', defaultWhitelistFn], ['merge-base', defaultWhitelistFn],
['remote', args => args[1] === '-v' || args[1] === 'get-url'], ['remote', args => args[1] === '-v' || args[1] === 'get-url'],
['rev-parse', defaultWhitelistFn], ['rev-parse', defaultWhitelistFn],
['shortlog', defaultWhitelistFn],
['show', defaultWhitelistFn], ['show', defaultWhitelistFn],
['show-ref', defaultWhitelistFn], ['show-ref', defaultWhitelistFn],
['stash', args => args[1] === 'list'], ['stash', args => args[1] === 'list'],

Loading…
Cancel
Save