diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d079f1..a7f05f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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/).
+## [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
### Added
diff --git a/README.md b/README.md
index 9c6c1cc..47df13d 100644
--- a/README.md
+++ b/README.md
@@ -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
- 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
- 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
- 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
- **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)
- 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)
+- 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)
- 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)
diff --git a/images/dark/icon-people.svg b/images/dark/icon-people.svg
new file mode 100644
index 0000000..62af7d3
--- /dev/null
+++ b/images/dark/icon-people.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/images/light/icon-people.svg b/images/light/icon-people.svg
new file mode 100644
index 0000000..3d7e760
--- /dev/null
+++ b/images/light/icon-people.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/package.json b/package.json
index 3464790..46b16ab 100644
--- a/package.json
+++ b/package.json
@@ -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",
"title": "Checkout Branch (via Terminal)",
"category": "GitLens"
@@ -3256,6 +3270,14 @@
"when": "false"
},
{
+ "command": "gitlens.views.contributor.addCoauthoredBy",
+ "when": "false"
+ },
+ {
+ "command": "gitlens.views.contributor.copyToClipboard",
+ "when": "false"
+ },
+ {
"command": "gitlens.views.terminalCheckoutBranch",
"when": "false"
},
@@ -4105,6 +4127,21 @@
"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",
"when": "viewItem =~ /gitlens:commit\\b/ && gitlens:hasRemotes",
"group": "inline@98"
diff --git a/src/container.ts b/src/container.ts
index 0c4a396..535ec04 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -6,7 +6,8 @@ import { GitCodeLensController } from './codelens/codeLensController';
import { Commands, ToggleFileBlameCommandArgs } from './commands';
import { AnnotationsToggleMode, Config, configuration, ConfigurationWillChangeEvent } from './configuration';
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 { Keyboard } from './keyboard';
import { Logger, TraceLevel } from './logger';
diff --git a/src/git/git.ts b/src/git/git.ts
index 8202c07..eb07399 100644
--- a/src/git/git.ts
+++ b/src/git/git.ts
@@ -811,6 +811,10 @@ export class Git {
return data.length === 0 ? undefined : data.trim();
}
+ static shortlog(repoPath: string) {
+ return git({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges');
+ }
+
static async show(
repoPath: string | undefined,
fileName: string,
diff --git a/src/git/gitService.ts b/src/git/gitService.ts
index bace35c..3ca92fa 100644
--- a/src/git/gitService.ts
+++ b/src/git/gitService.ts
@@ -6,6 +6,7 @@ import {
Disposable,
Event,
EventEmitter,
+ Extension,
extensions,
ProgressLocation,
Range,
@@ -18,7 +19,7 @@ import {
WorkspaceFoldersChangeEvent
} from 'vscode';
// 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 { CommandContext, DocumentSchemes, GlyphChars, setCommandContext } from '../constants';
import { Container } from '../container';
@@ -40,6 +41,7 @@ import {
GitBranchParser,
GitCommit,
GitCommitType,
+ GitContributor,
GitDiff,
GitDiffChunkLine,
GitDiffParser,
@@ -64,6 +66,7 @@ import {
} from './git';
import { GitUri } from './gitUri';
import { RemoteProviderFactory, RemoteProviders } from './remotes/factory';
+import { GitShortLogParser } from './parsers/parsers';
export * from './gitUri';
export * from './models/models';
@@ -1117,6 +1120,15 @@ export class GitService implements Disposable {
}
@log()
+ async getContributors(repoPath: string): Promise {
+ 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) {
let user = this._userMapCache.get(repoPath);
if (user != null) return user;
@@ -2218,18 +2230,27 @@ export class GitService implements Disposable {
static async initialize(): Promise {
// Try to use the same git as the built-in vscode git extension
let gitPath;
+ const gitApi = await GitService.getBuiltInGitApi();
+ if (gitApi !== undefined) {
+ gitPath = gitApi.git.path;
+ }
+
+ await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get('path'));
+ }
+
+ @log()
+ static async getBuiltInGitApi(): Promise {
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;
+ if (extension !== undefined) {
+ const gitExtension = extension.isActive ? extension.exports : await extension.activate();
+
+ return gitExtension.getAPI(1);
}
}
catch {}
- await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get('path'));
+ return undefined;
}
static getGitPath(): string {
diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts
index 2d8e7dd..70e1411 100644
--- a/src/git/models/commit.ts
+++ b/src/git/models/commit.ts
@@ -2,17 +2,11 @@
import { Uri } from 'vscode';
import { configuration, DateStyle, GravatarDefaultStyle } from '../../configuration';
import { Container } from '../../container';
-import { Dates, Strings } from '../../system';
+import { Dates } from '../../system';
import { CommitFormatter } from '../formatters/formatters';
import { Git } from '../git';
import { GitUri } from '../gitUri';
-
-const gravatarCache: Map = new Map();
-const missingGravatarHash = '00000000000000000000000000000000';
-
-export function clearGravatarCache() {
- gravatarCache.clear();
-}
+import { getGravatarUri } from '../../gravatar';
export interface GitAuthor {
name: string;
@@ -180,22 +174,11 @@ export abstract class GitCommit {
}
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() {
+ // eslint-disable-next-line no-template-curly-in-string
return CommitFormatter.fromTemplate('${message}', this, { truncateMessageAtNewLine: true });
}
diff --git a/src/git/models/contributor.ts b/src/git/models/contributor.ts
new file mode 100644
index 0000000..21c1377
--- /dev/null
+++ b/src/git/models/contributor.ts
@@ -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);
+ }
+}
diff --git a/src/git/models/models.ts b/src/git/models/models.ts
index 6eaa582..5a486b1 100644
--- a/src/git/models/models.ts
+++ b/src/git/models/models.ts
@@ -4,12 +4,14 @@ export * from './blame';
export * from './blameCommit';
export * from './branch';
export * from './commit';
+export * from './contributor';
export * from './diff';
export * from './file';
export * from './log';
export * from './logCommit';
export * from './remote';
export * from './repository';
+export * from './shortlog';
export * from './stash';
export * from './stashCommit';
export * from './status';
diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts
index 49e0352..3c97e30 100644
--- a/src/git/models/repository.ts
+++ b/src/git/models/repository.ts
@@ -18,7 +18,7 @@ import { configuration, RemotesConfig } from '../../configuration';
import { StarredRepositories, WorkspaceState } from '../../constants';
import { Container } from '../../container';
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 { RemoteProviderFactory, RemoteProviders } from '../remotes/factory';
@@ -272,6 +272,10 @@ export class Repository implements Disposable {
return Container.git.getChangedFilesCount(this.path, sha);
}
+ getContributors(): Promise {
+ return Container.git.getContributors(this.path);
+ }
+
async getLastFetched(): Promise {
const hasRemotes = await this.hasRemotes();
if (!hasRemotes || Container.vsls.isMaybeGuest) return 0;
diff --git a/src/git/models/shortlog.ts b/src/git/models/shortlog.ts
new file mode 100644
index 0000000..aadd78b
--- /dev/null
+++ b/src/git/models/shortlog.ts
@@ -0,0 +1,7 @@
+'use strict';
+import { GitContributor } from './contributor';
+
+export interface GitShortLog {
+ readonly repoPath: string;
+ readonly contributors: GitContributor[];
+}
diff --git a/src/git/parsers/parsers.ts b/src/git/parsers/parsers.ts
index bac644d..07d0952 100644
--- a/src/git/parsers/parsers.ts
+++ b/src/git/parsers/parsers.ts
@@ -5,6 +5,7 @@ export * from './branchParser';
export * from './diffParser';
export * from './logParser';
export * from './remoteParser';
+export * from './shortlogParser';
export * from './stashParser';
export * from './statusParser';
export * from './tagParser';
diff --git a/src/git/parsers/shortlogParser.ts b/src/git/parsers/shortlogParser.ts
new file mode 100644
index 0000000..2655f91
--- /dev/null
+++ b/src/git/parsers/shortlogParser.ts
@@ -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 };
+ }
+}
diff --git a/src/gravatar.ts b/src/gravatar.ts
new file mode 100644
index 0000000..32dd147
--- /dev/null
+++ b/src/gravatar.ts
@@ -0,0 +1,25 @@
+'use strict';
+import { Uri } from 'vscode';
+import { GravatarDefaultStyle } from './config';
+import { Strings } from './system';
+
+const gravatarCache: Map = 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;
+}
diff --git a/src/views/nodes.ts b/src/views/nodes.ts
index d703778..f518d5d 100644
--- a/src/views/nodes.ts
+++ b/src/views/nodes.ts
@@ -6,6 +6,8 @@ export * from './nodes/branchNode';
export * from './nodes/branchTrackingStatusNode';
export * from './nodes/commitFileNode';
export * from './nodes/commitNode';
+export * from './nodes/contributorNode';
+export * from './nodes/contributorsNode';
export * from './nodes/fileHistoryNode';
export * from './nodes/fileHistoryTrackerNode';
export * from './nodes/folderNode';
diff --git a/src/views/nodes/contributorNode.ts b/src/views/nodes/contributorNode.ts
new file mode 100644
index 0000000..267888f
--- /dev/null
+++ b/src/views/nodes/contributorNode.ts
@@ -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 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 {
+ 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;
+ }
+}
diff --git a/src/views/nodes/contributorsNode.ts b/src/views/nodes/contributorsNode.ts
new file mode 100644
index 0000000..206e5f0
--- /dev/null
+++ b/src/views/nodes/contributorsNode.ts
@@ -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 {
+ 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 {
+ 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;
+ }
+}
diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts
index c856817..67846bf 100644
--- a/src/views/nodes/repositoryNode.ts
+++ b/src/views/nodes/repositoryNode.ts
@@ -22,6 +22,7 @@ import { StashesNode } from './stashesNode';
import { StatusFilesNode } from './statusFilesNode';
import { TagsNode } from './tagsNode';
import { ResourceType, SubscribeableViewNode, ViewNode } from './viewNode';
+import { ContributorsNode } from './contributorsNode';
const hasTimeRegex = /[hHm]/;
@@ -78,6 +79,7 @@ export class RepositoryNode extends SubscribeableViewNode {
children.push(
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 StashesNode(this.uri, this.view, this, this.repo),
new TagsNode(this.uri, this.view, this, this.repo)
diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts
index 37559e7..08a7b0b 100644
--- a/src/views/nodes/viewNode.ts
+++ b/src/views/nodes/viewNode.ts
@@ -19,6 +19,8 @@ export enum ResourceType {
ComparePicker = 'gitlens:compare:picker',
ComparePickerWithRef = 'gitlens:compare:picker:ref',
CompareResults = 'gitlens:compare:results',
+ Contributor = 'gitlens:contributor',
+ Contributors = 'gitlens:contributors',
File = 'gitlens:file',
FileHistory = 'gitlens:history:file',
Folder = 'gitlens:folder',
diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts
index 74d1f01..74a9f74 100644
--- a/src/views/viewCommands.ts
+++ b/src/views/viewCommands.ts
@@ -1,6 +1,6 @@
'use strict';
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 {
Commands,
DiffWithCommandArgs,
@@ -34,6 +34,8 @@ import {
ViewRefNode,
viewSupportsNodeDismissal
} from './nodes';
+import { ContributorNode } from './nodes/contributorNode';
+import { Strings } from '../system/string';
export interface RefreshNodeCommandArgs {
maxCount?: number;
@@ -83,6 +85,9 @@ export class ViewCommands implements Disposable {
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.openChangesWithWorking', this.openChangesWithWorking, this);
commands.registerCommand('gitlens.views.openFile', this.openFile, this);
@@ -139,6 +144,44 @@ export class ViewCommands implements Disposable {
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) {
if (node instanceof RemoteNode) return node.fetch();
if (node instanceof RepositoryNode) return node.fetch();
diff --git a/src/vsls/host.ts b/src/vsls/host.ts
index db156ad..5519ec0 100644
--- a/src/vsls/host.ts
+++ b/src/vsls/host.ts
@@ -34,6 +34,7 @@ const gitWhitelist = new Map boolean>([
['merge-base', defaultWhitelistFn],
['remote', args => args[1] === '-v' || args[1] === 'get-url'],
['rev-parse', defaultWhitelistFn],
+ ['shortlog', defaultWhitelistFn],
['show', defaultWhitelistFn],
['show-ref', defaultWhitelistFn],
['stash', args => args[1] === 'list'],