瀏覽代碼

Closes #1489: adds contributor stats

main
Eric Amodio 3 年之前
父節點
當前提交
9696ac69c0
共有 18 個文件被更改,包括 204 次插入42 次删除
  1. +3
    -0
      CHANGELOG.md
  2. +1
    -0
      README.md
  3. +34
    -0
      package.json
  4. +1
    -0
      src/config.ts
  5. +4
    -2
      src/git/git.ts
  6. +18
    -9
      src/git/gitService.ts
  7. +5
    -0
      src/git/models/contributor.ts
  8. +3
    -2
      src/git/models/repository.ts
  9. +42
    -2
      src/git/parsers/shortlogParser.ts
  10. +1
    -0
      src/views/branchesView.ts
  11. +1
    -0
      src/views/commitsView.ts
  12. +34
    -17
      src/views/contributorsView.ts
  13. +50
    -8
      src/views/nodes/contributorNode.ts
  14. +3
    -1
      src/views/nodes/contributorsNode.ts
  15. +1
    -0
      src/views/nodes/fileHistoryNode.ts
  16. +1
    -0
      src/views/nodes/lineHistoryNode.ts
  17. +1
    -1
      src/views/nodes/repositoryNode.ts
  18. +1
    -0
      src/views/remotesView.ts

+ 3
- 0
CHANGELOG.md 查看文件

@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Adds the ability to filter comparisons to show only either the left-side or right-side file differences
- Adds the _Open Folder History_ command to root folders — closes [#1505](https://github.com/eamodio/vscode-gitlens/issues/1505)
- Adds the ability to show contributor statistics, files changed as well as lines added and deleted (can take a while to compute depending on the repository) — closes [#1489](https://github.com/eamodio/vscode-gitlens/issues/1489)
- Adds a _Show Statistics_ / _Hide Statistics_ toggle to the `...` menu of the _Contributors_ view
- Adds a `gitlens.views.contributors.showStatistics` settings to specify whether to show contributor statistics in the _Contributors_ view
### Changed

+ 1
- 0
README.md 查看文件

@ -884,6 +884,7 @@ See also [View Settings](#view-settings- 'Jump to the View settings')
| `gitlens.views.contributors.pullRequests.enabled` | Specifies whether to query for pull requests associated with the current branch and commits in the _Contributors_ view. Requires a connection to a supported remote service (e.g. GitHub) |
| `gitlens.views.contributors.pullRequests.showForCommits` | Specifies whether to show pull requests (if any) associated with the current branch in the _Contributors_ view. Requires a connection to a supported remote service (e.g. GitHub) |
| `gitlens.views.contributors.showAllBranches` | Specifies whether to show commits from all branches in the _Contributors_ view |
| `gitlens.views.contributors.showStatistics` | Specifies whether to show contributor statistics in the _Contributors_ view. This can take a while to compute depending on the repository size |
## Search & Compare View Settings [#](#search-&-compare-view-settings- 'Search & Compare View Settings')

+ 34
- 0
package.json 查看文件

@ -1978,6 +1978,12 @@
"markdownDescription": "Specifies whether to show commits from all branches in the _Contributors_ view",
"scope": "window"
},
"gitlens.views.contributors.showStatistics": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to show contributor statistics in the _Contributors_ view. This can take a while to compute depending on the repository size",
"scope": "window"
},
"gitlens.views.defaultItemLimit": {
"type": "number",
"default": 10,
@ -4628,6 +4634,16 @@
"category": "GitLens"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOn",
"title": "Show Statistics",
"category": "GitLens"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOff",
"title": "Hide Statistics",
"category": "GitLens"
},
{
"command": "gitlens.views.fileHistory.changeBase",
"title": "Change Base...",
"category": "GitLens",
@ -6190,6 +6206,14 @@
"when": "false"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOn",
"when": "false"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOff",
"when": "false"
},
{
"command": "gitlens.views.fileHistory.changeBase",
"when": "false"
},
@ -7165,6 +7189,16 @@
"group": "5_gitlens@0"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOn",
"when": "view =~ /^gitlens\\.views\\.contributors/ && !config.gitlens.views.contributors.showStatistics",
"group": "5_gitlens@1"
},
{
"command": "gitlens.views.contributors.setShowStatisticsOff",
"when": "view =~ /^gitlens\\.views\\.contributors/ && config.gitlens.views.contributors.showStatistics",
"group": "5_gitlens@1"
},
{
"command": "gitlens.views.fileHistory.setEditorFollowingOn",
"when": "view =~ /^gitlens\\.views\\.fileHistory/ && gitlens:views:fileHistory:canPin && !gitlens:views:fileHistory:editorFollowing",
"group": "navigation@10"

+ 1
- 0
src/config.ts 查看文件

@ -563,6 +563,7 @@ export interface ContributorsViewConfig {
showForCommits: boolean;
};
showAllBranches: boolean;
showStatistics: boolean;
}
export interface FileHistoryViewConfig {

+ 4
- 2
src/git/git.ts 查看文件

@ -753,7 +753,7 @@ export namespace Git {
}: {
all?: boolean;
authors?: string[];
format?: 'default' | 'refs' | 'shortlog';
format?: 'default' | 'refs' | 'shortlog' | 'shortlog+stats';
limit?: number;
merges?: boolean;
ordering?: string | null;
@ -767,7 +767,7 @@ export namespace Git {
`--format=${
format === 'refs'
? GitLogParser.simpleRefs
: format === 'shortlog'
: format === 'shortlog' || format === 'shortlog+stats'
? GitLogParser.shortlog
: GitLogParser.defaultFormat
}`,
@ -778,6 +778,8 @@ export namespace Git {
if (format === 'default') {
params.push('--name-status');
} else if (format === 'shortlog+stats') {
params.push('--shortstat');
}
if (ordering) {

+ 18
- 9
src/git/gitService.ts 查看文件

@ -201,10 +201,11 @@ export class GitService implements Disposable {
if (e.changed(RepositoryChange.Heads, RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) {
this._branchesCache.delete(repo.path);
this._contributorsCache.delete(repo.path);
this._contributorsCache.delete(`stats|${repo.path}`);
}
if (e.changed(RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) {
this._remotesWithApiProviderCache.clear();
}
if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) {
this._remotesWithApiProviderCache.clear();
}
if (e.changed(RepositoryChange.Index, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any)) {
@ -1530,21 +1531,29 @@ export class GitService implements Disposable {
}
@log()
async getContributors(repoPath: string, options?: { all?: boolean; ref?: string }): Promise<GitContributor[]> {
async getContributors(
repoPath: string,
options?: { all?: boolean; ref?: string; stats?: boolean },
): Promise<GitContributor[]> {
if (repoPath == null) return [];
let contributors = this.useCaching ? this._contributorsCache.get(repoPath) : undefined;
const key = options?.stats ? `stats|${repoPath}` : repoPath;
let contributors = this.useCaching ? this._contributorsCache.get(key) : undefined;
if (contributors == null) {
async function load(this: GitService) {
try {
const currentUser = await this.getCurrentUser(repoPath);
const data = await Git.log(repoPath, options?.ref, { all: options?.all, format: 'shortlog' });
const data = await Git.log(repoPath, options?.ref, {
all: options?.all,
format: options?.stats ? 'shortlog+stats' : 'shortlog',
});
const shortlog = GitShortLogParser.parseFromLog(data, repoPath, currentUser);
return shortlog != null ? shortlog.contributors : [];
} catch (ex) {
this._contributorsCache.delete(repoPath);
this._contributorsCache.delete(key);
return [];
}
@ -1553,10 +1562,10 @@ export class GitService implements Disposable {
contributors = load.call(this);
if (this.useCaching) {
this._contributorsCache.set(repoPath, contributors);
this._contributorsCache.set(key, contributors);
if (!(await this.getRepository(repoPath))?.supportsChangeEvents) {
this._contributorsCache.delete(repoPath);
this._contributorsCache.delete(key);
}
}
}

+ 5
- 0
src/git/models/contributor.ts 查看文件

@ -68,6 +68,11 @@ export class GitContributor {
public readonly email: string,
public readonly count: number,
public readonly date: Date,
public readonly stats?: {
files: number;
additions: number;
deletions: number;
},
public readonly current: boolean = false,
) {}

+ 3
- 2
src/git/models/repository.ts 查看文件

@ -64,6 +64,7 @@ export const enum RepositoryChange {
Merge = 'merge',
Rebase = 'rebase',
Remotes = 'remotes',
RemoteProviders = 'providers',
Stash = 'stash',
/*
* Union of Cherry, Merge, and Rebase
@ -571,7 +572,7 @@ export class Repository implements Disposable {
return Container.git.getCommit(this.path, ref);
}
getContributors(options?: { all?: boolean; ref?: string }): Promise<GitContributor[]> {
getContributors(options?: { all?: boolean; ref?: string; stats?: boolean }): Promise<GitContributor[]> {
return Container.git.getContributors(this.path, options);
}
@ -635,7 +636,7 @@ export class Repository implements Disposable {
...Iterables.filterMap(await remotes, r => {
if (!RichRemoteProvider.is(r.provider)) return undefined;
return r.provider.onDidChange(() => this.fireChange(RepositoryChange.Remotes));
return r.provider.onDidChange(() => this.fireChange(RepositoryChange.RemoteProviders));
}),
);
}

+ 42
- 2
src/git/parsers/shortlogParser.ts 查看文件

@ -3,6 +3,8 @@ import { GitContributor, GitShortLog, GitUser } from '../git';
import { debug } from '../../system';
const shortlogRegex = /^(.*?)\t(.*?) <(.*?)>$/gm;
const shortstatRegex =
/(?<files>\d+) files? changed(?:, (?<additions>\d+) insertions?\(\+\))?(?:, (?<deletions>\d+) deletions?\(-\))?/;
export class GitShortLogParser {
@debug({ args: false, singleLine: true })
@ -48,12 +50,39 @@ export class GitShortLogParser {
email: string;
count: number;
timestamp: number;
stats?: {
files: number;
additions: number;
deletions: number;
};
};
const contributors = new Map<string, Contributor>();
for (const line of data.trim().split('\n')) {
const [sha, author, email, date] = line.trim().split('\0');
const lines = data.trim().split('\n');
for (let i = 0; i < lines.length; i++) {
const [sha, author, email, date] = lines[i].trim().split('\0');
let stats:
| {
files: number;
additions: number;
deletions: number;
}
| undefined;
if (lines[i + 1] === '') {
i += 2;
const match = shortstatRegex.exec(lines[i]);
if (match?.groups != null) {
const { files, additions, deletions } = match.groups;
stats = {
files: Number(files || 0),
additions: Number(additions || 0),
deletions: Number(deletions || 0),
};
}
}
const timestamp = Number(date);
@ -65,9 +94,19 @@ export class GitShortLogParser {
email: email,
count: 1,
timestamp: timestamp,
stats: stats,
});
} else {
contributor.count++;
if (stats != null) {
if (contributor.stats == null) {
contributor.stats = stats;
} else {
contributor.stats.files += stats.files;
contributor.stats.additions += stats.additions;
contributor.stats.deletions += stats.deletions;
}
}
if (timestamp > contributor.timestamp) {
contributor.timestamp = timestamp;
}
@ -88,6 +127,7 @@ export class GitShortLogParser {
c.email,
c.count,
new Date(Number(c.timestamp) * 1000),
c.stats,
currentUser != null
? currentUser.name === c.name && currentUser.email === c.email
: false,

+ 1
- 0
src/views/branchesView.ts 查看文件

@ -54,6 +54,7 @@ export class BranchesRepositoryNode extends RepositoryFolderNode
RepositoryChange.Heads,
RepositoryChange.Index,
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChange.Status,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,

+ 1
- 0
src/views/commitsView.ts 查看文件

@ -110,6 +110,7 @@ export class CommitsRepositoryNode extends RepositoryFolderNode
RepositoryChange.Heads,
RepositoryChange.Index,
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChange.Status,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,

+ 34
- 17
src/views/contributorsView.ts 查看文件

@ -70,21 +70,23 @@ export class ContributorsViewNode extends ViewNode {
this.view.description = `${Strings.pad(GlyphChars.Warning, 0, 2)}Auto-refresh unavailable`;
}
const all = Container.config.views.contributors.showAllBranches;
let ref: string | undefined;
// If we aren't getting all branches, get the upstream of the current branch if there is one
if (!all) {
try {
const branch = await Container.git.getBranch(this.uri.repoPath);
if (branch?.upstream?.name != null && !branch.upstream.missing) {
ref = '@{u}';
}
} catch {}
}
const contributors = await child.repo.getContributors({ all: all, ref: ref });
if (contributors.length === 0) {
const children = await child.getChildren();
// const all = Container.config.views.contributors.showAllBranches;
// let ref: string | undefined;
// // If we aren't getting all branches, get the upstream of the current branch if there is one
// if (!all) {
// try {
// const branch = await Container.git.getBranch(this.uri.repoPath);
// if (branch?.upstream?.name != null && !branch.upstream.missing) {
// ref = '@{u}';
// }
// } catch {}
// }
// const contributors = await child.repo.getContributors({ all: all, ref: ref });
if (children.length === 0) {
this.view.message = 'No contributors could be found.';
this.view.title = 'Contributors';
@ -94,9 +96,9 @@ export class ContributorsViewNode extends ViewNode {
}
this.view.message = undefined;
this.view.title = `Contributors (${contributors.length})`;
this.view.title = `Contributors (${children.length})`;
return child.getChildren();
return children;
}
return this.children;
@ -183,6 +185,17 @@ export class ContributorsView extends ViewBase
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOn'), () => this.setShowAvatars(true), this);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
commands.registerCommand(
this.getQualifiedCommand('setShowStatisticsOn'),
() => this.setShowStatistics(true),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setShowStatisticsOff'),
() => this.setShowStatistics(false),
this,
);
}
protected override filterConfigurationChanged(e: ConfigurationChangeEvent) {
@ -214,4 +227,8 @@ export class ContributorsView extends ViewBase
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective(`views.${this.configKey}.avatars` as const, enabled);
}
private setShowStatistics(enabled: boolean) {
return configuration.updateEffective(`views.${this.configKey}.showStatistics` as const, enabled);
}
}

+ 50
- 8
src/views/nodes/contributorNode.ts 查看文件

@ -1,5 +1,5 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { MarkdownString, TreeItem, TreeItemCollapsibleState, window } from 'vscode';
import { CommitNode } from './commitNode';
import { LoadMoreNode, MessageNode } from './common';
import { GlyphChars } from '../../constants';
@ -13,6 +13,7 @@ import { RepositoryNode } from './repositoryNode';
import { debug, gate, Iterables, Strings } from '../../system';
import { ContextValues, PageableViewNode, ViewNode } from './viewNode';
import { ContactPresence } from '../../vsls/vsls';
import { getPresenceDataUri } from '../../avatars';
export class ContributorNode extends ViewNode<ContributorsView | RepositoriesView> implements PageableViewNode {
static key = ':contributor';
@ -79,19 +80,60 @@ export class ContributorNode extends ViewNode
? `${presence.statusText} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} `
: ''
}${this.contributor.email}`;
item.tooltip = `${this.contributor.name}${presence != null ? ` (${presence.statusText})` : ''}\n${
this.contributor.email
}\n${Strings.pluralize(
'commit',
this.contributor.count,
)}\nLast commit ${this.contributor.formatDateFromNow()} (${this.contributor.formatDate()})`;
let avatarUri;
let avatarMarkdown;
if (this.view.config.avatars) {
item.iconPath = await this.contributor.getAvatarUri({
const size = Container.config.hovers.avatarSize;
avatarUri = await this.contributor.getAvatarUri({
defaultStyle: Container.config.defaultGravatarsStyle,
size: size,
});
if (presence != null) {
const title = `${this.contributor.count ? 'You are' : `${this.contributor.name} is`} ${
presence.status === 'dnd' ? 'in ' : ''
}${presence.statusText.toLocaleLowerCase()}`;
avatarMarkdown = `![${title}](${avatarUri.toString(
true,
)}|width=${size},height=${size} "${title}")![${title}](${getPresenceDataUri(
presence.status,
)} "${title}")`;
} else {
avatarMarkdown = `![${this.contributor.name}](${avatarUri.toString(
true,
)}|width=${size},height=${size} "${this.contributor.name}")`;
}
}
const numberFormatter = new Intl.NumberFormat();
const stats =
this.contributor.stats != null
? `\\\n${Strings.pluralize('file', this.contributor.stats.files, {
number: numberFormatter.format(this.contributor.stats.files),
})} changed, ${Strings.pluralize('addition', this.contributor.stats.additions, {
number: numberFormatter.format(this.contributor.stats.additions),
})}, ${Strings.pluralize('deletion', this.contributor.stats.deletions, {
number: numberFormatter.format(this.contributor.stats.deletions),
})}`
: '';
item.tooltip = new MarkdownString(
`${avatarMarkdown != null ? avatarMarkdown : ''} &nbsp;__[${this.contributor.name}](mailto:${
this.contributor.email
} "Email ${this.contributor.name} (${
this.contributor.email
})")__ \\\nLast commit ${this.contributor.formatDateFromNow()} (${this.contributor.formatDate()})\n\n${Strings.pluralize(
'commit',
this.contributor.count,
{ number: numberFormatter.format(this.contributor.count) },
)}${stats}`,
);
item.iconPath = avatarUri;
return item;
}

+ 3
- 1
src/views/nodes/contributorsNode.ts 查看文件

@ -49,7 +49,9 @@ export class ContributorsNode extends ViewNode
} catch {}
}
const contributors = await this.repo.getContributors({ all: all, ref: ref });
const stats = Container.config.views.contributors.showStatistics;
const contributors = await this.repo.getContributors({ all: all, ref: ref, stats: stats });
if (contributors.length === 0) return [new MessageNode(this.view, this, 'No contributors could be found.')];
GitContributor.sort(contributors);

+ 1
- 0
src/views/nodes/fileHistoryNode.ts 查看文件

@ -192,6 +192,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl
RepositoryChange.Index,
RepositoryChange.Heads,
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChange.Status,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,

+ 1
- 0
src/views/nodes/lineHistoryNode.ts 查看文件

@ -281,6 +281,7 @@ export class LineHistoryNode
RepositoryChange.Index,
RepositoryChange.Heads,
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChange.Status,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,

+ 1
- 1
src/views/nodes/repositoryNode.ts 查看文件

@ -421,7 +421,7 @@ export class RepositoryNode extends SubscribeableViewNode {
return;
}
if (e.changed(RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) {
if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) {
const node = this._children.find(c => c instanceof RemotesNode);
if (node != null) {
void this.view.triggerNodeChange(node);

+ 1
- 0
src/views/remotesView.ts 查看文件

@ -49,6 +49,7 @@ export class RemotesRepositoryNode extends RepositoryFolderNode
return e.changed(
RepositoryChange.Config,
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,
);

Loading…
取消
儲存