Browse Source

Adds commit filtering to comparisons

main
Eric Amodio 1 year ago
parent
commit
7c906b6eb2
11 changed files with 271 additions and 78 deletions
  1. +57
    -22
      package.json
  2. +8
    -3
      src/env/node/git/git.ts
  3. +2
    -1
      src/env/node/git/localGitProvider.ts
  4. +5
    -1
      src/git/gitProvider.ts
  5. +2
    -1
      src/git/gitProviderService.ts
  6. +1
    -0
      src/plus/github/githubGitProvider.ts
  7. +48
    -15
      src/views/nodes/compareBranchNode.ts
  8. +45
    -15
      src/views/nodes/compareResultsNode.ts
  9. +6
    -1
      src/views/nodes/resultsCommitsNode.ts
  10. +40
    -19
      src/views/nodes/viewNode.ts
  11. +57
    -0
      src/views/viewCommands.ts

+ 57
- 22
package.json View File

@ -7207,6 +7207,18 @@
"icon": "$(list-flat)"
},
{
"command": "gitlens.views.setResultsCommitsFilterAuthors",
"title": "Filter Commits by Author...",
"category": "GitLens",
"icon": "$(filter)"
},
{
"command": "gitlens.views.setResultsCommitsFilterOff",
"title": "Clear Filter",
"category": "GitLens",
"icon": "$(filter-filled)"
},
{
"command": "gitlens.views.searchAndCompare.setShowAvatarsOn",
"title": "Show Avatars",
"category": "GitLens"
@ -7221,7 +7233,7 @@
"title": "Swap Comparison",
"category": "GitLens",
"icon": "$(arrow-swap)",
"enablement": "viewItem =~ /gitlens:compare:results(?!:)\\b(?!.*?\\b\\+working\\b)/"
"enablement": "viewItem =~ /gitlens:compare:results\\b(?!.*?\\b\\+working\\b)/"
},
{
"command": "gitlens.views.searchAndCompare.setFilesFilterOnLeft",
@ -9890,6 +9902,14 @@
"when": "false"
},
{
"command": "gitlens.views.setResultsCommitsFilterAuthors",
"when": "false"
},
{
"command": "gitlens.views.setResultsCommitsFilterOff",
"when": "false"
},
{
"command": "gitlens.views.searchAndCompare.setShowAvatarsOn",
"when": "false"
},
@ -12828,7 +12848,7 @@
{
"command": "gitlens.views.editNode",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+comparing\\b)/",
"group": "inline@98"
"group": "inline@96"
},
{
"command": "gitlens.views.setBranchComparisonToWorking",
@ -12841,21 +12861,26 @@
"group": "inline@2"
},
{
"command": "gitlens.views.editNode",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+comparing\\b)/",
"group": "1_gitlens@1"
},
{
"command": "gitlens.views.setBranchComparisonToWorking",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+root\\b)(?=.*?\\b\\+current\\b)(?=.*?\\b\\+branch\\b)/",
"group": "1_gitlens@2"
"group": "1_gitlens@1"
},
{
"command": "gitlens.views.setBranchComparisonToBranch",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+root\\b)(?=.*?\\b\\+current\\b)(?=.*?\\b\\+working\\b)/",
"group": "1_gitlens@1"
},
{
"command": "gitlens.views.editNode",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+comparing\\b)/",
"group": "1_gitlens@2"
},
{
"command": "gitlens.views.clearNode",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+comparing\\b)/",
"group": "1_gitlens@3"
},
{
"command": "gitlens.views.branches.setShowBranchComparisonOff",
"when": "view =~ /gitlens\\.views\\.branches\\b/ && viewItem =~ /gitlens:compare:branch\\b/",
"group": "8_gitlens_toggles@1"
@ -12876,13 +12901,8 @@
"group": "8_gitlens_toggles@99"
},
{
"command": "gitlens.views.clearNode",
"when": "viewItem =~ /gitlens:compare:branch\\b(?=.*?\\b\\+comparing\\b)/",
"group": "9_gitlens@1"
},
{
"command": "gitlens.views.searchAndCompare.swapComparison",
"when": "viewItem =~ /gitlens:compare:results(?!:)\\b/",
"when": "viewItem =~ /gitlens:compare:results\\b/",
"group": "inline@1"
},
{
@ -12897,32 +12917,47 @@
},
{
"command": "gitlens.views.refreshNode",
"when": "viewItem =~ /gitlens:compare:(branch(?=.*?\\b\\+comparing\\b)|results(?!:))\\b/",
"when": "viewItem =~ /gitlens:compare:(branch(?=.*?\\b\\+comparing\\b)|results)\\b/",
"group": "inline@97"
},
{
"command": "gitlens.views.refreshNode",
"when": "viewItem =~ /gitlens:search:results(?!:)\\b/",
"when": "viewItem =~ /gitlens:search:results\\b/",
"group": "inline@97"
},
{
"command": "gitlens.views.setResultsCommitsFilterOff",
"when": "viewItem =~ /gitlens:compare:(results|branch)\\b(?=.*?\\b\\+filtered\\b)/",
"group": "inline@96"
},
{
"command": "gitlens.views.searchAndCompare.swapComparison",
"when": "viewItem =~ /gitlens:compare:results(?!:)\\b(?!.*?\\b\\+working\\b)/",
"when": "viewItem =~ /gitlens:compare:results\\b(?!.*?\\b\\+working\\b)/",
"group": "1_gitlens_actions@2"
},
{
"command": "gitlens.views.openDirectoryDiff",
"when": "viewItem =~ /gitlens:compare:results(?!:)\\b/",
"when": "viewItem =~ /gitlens:compare:results\\b/",
"group": "2_gitlens_quickopen@1"
},
{
"command": "gitlens.views.setResultsCommitsFilterOff",
"when": "viewItem =~ /gitlens:compare:(results|branch)\\b(?=.*?\\b\\+filtered\\b)/",
"group": "7_gitlens_filter@1"
},
{
"command": "gitlens.views.setResultsCommitsFilterAuthors",
"when": "viewItem =~ /gitlens:compare:(results|branch)\\b/",
"group": "7_gitlens_filter@2"
},
{
"command": "gitlens.views.editNode",
"when": "viewItem =~ /gitlens:search:results(?!:)\\b/",
"when": "viewItem =~ /gitlens:search:results\\b/",
"group": "inline@1"
},
{
"command": "gitlens.views.editNode",
"when": "viewItem =~ /gitlens:search:results(?!:)\\b/",
"when": "viewItem =~ /gitlens:search:results\\b/",
"group": "1_gitlens_actions@1"
},
{
@ -13102,7 +13137,7 @@
{
"command": "gitlens.views.dismissNode",
"when": "viewItem =~ /gitlens:(compare:picker:ref|(compare|search):results(?!:)\\b)\\b(?!:(commits|files))/",
"group": "8_gitlens_actions@98"
"group": "1_gitlens_actions@98"
},
{
"command": "gitlens.views.collapseNode",
@ -13117,7 +13152,7 @@
{
"command": "gitlens.views.refreshNode",
"when": "viewItem =~ /gitlens:(?!(file|message|date-marker)\\b)/",
"group": "9_gitlens@99"
"group": "9_gitlens_z@99"
},
{
"command": "gitlens.views.loadAllChildren",

+ 8
- 3
src/env/node/git/git.ts View File

@ -1660,12 +1660,17 @@ export class Git {
async rev_list__left_right(
repoPath: string,
refs: string[],
authors?: GitUser[] | undefined,
): Promise<{ ahead: number; behind: number } | undefined> {
const params = ['rev-list', '--left-right', '--count'];
if (authors?.length) {
params.push(...authors.map(a => `--author=^${a.name} <${a.email}>$`));
}
const data = await this.git<string>(
{ cwd: repoPath, errors: GitErrorHandling.Ignore },
'rev-list',
'--left-right',
'--count',
...params,
...refs,
'--',
);

+ 2
- 1
src/env/node/git/localGitProvider.ts View File

@ -1396,8 +1396,9 @@ export class LocalGitProvider implements GitProvider, Disposable {
getAheadBehindCommitCount(
repoPath: string,
refs: string[],
options?: { authors?: GitUser[] | undefined },
): Promise<{ ahead: number; behind: number } | undefined> {
return this.git.rev_list__left_right(repoPath, refs);
return this.git.rev_list__left_right(repoPath, refs, options?.authors);
}
@gate<LocalGitProvider['getBlame']>((u, d) => `${u.toString()}|${d?.isDirty}`)

+ 5
- 1
src/git/gitProvider.ts View File

@ -172,7 +172,11 @@ export interface GitProvider extends Disposable {
},
): Promise<void>;
findRepositoryUri(uri: Uri, isDirectory?: boolean): Promise<Uri | undefined>;
getAheadBehindCommitCount(repoPath: string, refs: string[]): Promise<{ ahead: number; behind: number } | undefined>;
getAheadBehindCommitCount(
repoPath: string,
refs: string[],
options?: { authors?: GitUser[] | undefined },
): Promise<{ ahead: number; behind: number } | undefined>;
/**
* Returns the blame of a file
* @param uri Uri of the file to blame

+ 2
- 1
src/git/gitProviderService.ts View File

@ -1403,9 +1403,10 @@ export class GitProviderService implements Disposable {
getAheadBehindCommitCount(
repoPath: string | Uri,
refs: string[],
options?: { authors?: GitUser[] | undefined },
): Promise<{ ahead: number; behind: number } | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getAheadBehindCommitCount(path, refs);
return provider.getAheadBehindCommitCount(path, refs, options);
}
@log<GitProviderService['getBlame']>({ args: { 1: d => d?.isDirty } })

+ 1
- 0
src/plus/github/githubGitProvider.ts View File

@ -521,6 +521,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
async getAheadBehindCommitCount(
_repoPath: string,
_refs: string[],
_options?: { authors?: GitUser[] | undefined },
): Promise<{ ahead: number; behind: number } | undefined> {
return undefined;
}

+ 48
- 15
src/views/nodes/compareBranchNode.ts View File

@ -6,6 +6,7 @@ import { GlyphChars } from '../../constants';
import type { GitUri } from '../../git/gitUri';
import type { GitBranch } from '../../git/models/branch';
import { createRevisionRange, shortenRevision } from '../../git/models/reference';
import type { GitUser } from '../../git/models/user';
import { CommandQuickPickItem } from '../../quickpicks/items/common';
import { showReferencePicker } from '../../quickpicks/referencePicker';
import { gate } from '../../system/decorators/gate';
@ -27,7 +28,15 @@ import { ResultsFilesNode } from './resultsFilesNode';
import type { ViewNode } from './viewNode';
import { ContextValues, getViewNodeId, SubscribeableViewNode } from './viewNode';
export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', ViewsWithBranches | WorktreesView> {
type State = {
filterCommits: GitUser[] | undefined;
};
export class CompareBranchNode extends SubscribeableViewNode<
'compare-branch',
ViewsWithBranches | WorktreesView,
State
> {
private _children: ViewNode[] | undefined;
private _compareWith: StoredBranchComparison | undefined;
@ -65,6 +74,19 @@ export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', V
};
}
private _isFiltered: boolean | undefined;
private get filterByAuthors(): GitUser[] | undefined {
const authors = this.getState('filterCommits');
const isFiltered = Boolean(authors?.length);
if (this._isFiltered != null && this._isFiltered !== isFiltered) {
this.updateContext({ comparisonFiltered: isFiltered });
}
this._isFiltered = isFiltered;
return authors;
}
get repoPath(): string {
return this.branch.repoPath;
}
@ -87,9 +109,11 @@ export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', V
const ahead = this.ahead;
const behind = this.behind;
const aheadBehindCounts = await this.view.container.git.getAheadBehindCommitCount(this.branch.repoPath, [
createRevisionRange(behind.ref1, behind.ref2, '...'),
]);
const aheadBehindCounts = await this.view.container.git.getAheadBehindCommitCount(
this.branch.repoPath,
[createRevisionRange(behind.ref1, behind.ref2, '...')],
{ authors: this.filterByAuthors },
);
const mergeBase =
(await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2, {
forkPoint: true,
@ -138,17 +162,23 @@ export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', V
expand: false,
},
),
new ResultsFilesNode(
this.view,
this,
this.repoPath,
this._compareWith.ref || 'HEAD',
this.compareWithWorkingTree ? '' : this.branch.ref,
this.getFilesQuery.bind(this),
undefined,
{ expand: false },
),
];
// Can't support showing files when commits are filtered
if (!this.filterByAuthors?.length) {
this._children.push(
new ResultsFilesNode(
this.view,
this,
this.repoPath,
this._compareWith.ref || 'HEAD',
this.compareWithWorkingTree ? '' : this.branch.ref,
this.getFilesQuery.bind(this),
undefined,
{ expand: false },
),
);
}
}
return this._children;
}
@ -179,7 +209,9 @@ export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', V
item.id = this.id;
item.contextValue = `${ContextValues.CompareBranch}${this.branch.current ? '+current' : ''}+${
this.comparisonType
}${this._compareWith == null ? '' : '+comparing'}${this.root ? '+root' : ''}`;
}${this._compareWith == null ? '' : '+comparing'}${this.root ? '+root' : ''}${
this.filterByAuthors?.length ? '+filtered' : ''
}`;
if (this._compareWith == null) {
item.command = {
@ -338,6 +370,7 @@ export class CompareBranchNode extends SubscribeableViewNode<'compare-branch', V
const log = await this.view.container.git.getLog(repoPath, {
limit: limit,
ref: range,
authors: this.filterByAuthors,
});
const results: Mutable<Partial<CommitsQueryResults>> = {

+ 45
- 15
src/views/nodes/compareResultsNode.ts View File

@ -4,6 +4,7 @@ import { md5 } from '@env/crypto';
import type { StoredNamedRef } from '../../constants';
import { GitUri } from '../../git/gitUri';
import { createRevisionRange, shortenRevision } from '../../git/models/reference';
import type { GitUser } from '../../git/models/user';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { getSettledValue } from '../../system/promise';
@ -19,7 +20,11 @@ import { ContextValues, getViewNodeId, SubscribeableViewNode } from './viewNode'
let instanceId = 0;
export class CompareResultsNode extends SubscribeableViewNode<'compare-results', SearchAndCompareView> {
type State = {
filterCommits: GitUser[] | undefined;
};
export class CompareResultsNode extends SubscribeableViewNode<'compare-results', SearchAndCompareView, State> {
private _instanceId: number;
constructor(
@ -80,6 +85,19 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
return this._compareWith;
}
private _isFiltered: boolean | undefined;
private get filterByAuthors(): GitUser[] | undefined {
const authors = this.getState('filterCommits');
const isFiltered = Boolean(authors?.length);
if (this._isFiltered != null && this._isFiltered !== isFiltered) {
this.updateContext({ comparisonFiltered: isFiltered });
}
this._isFiltered = isFiltered;
return authors;
}
protected override subscribe(): Disposable | Promise<Disposable | undefined> | undefined {
return this.view.onDidChangeNodesCheckedState(this.onNodesCheckedStateChanged, this);
}
@ -102,9 +120,12 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
const ahead = this.ahead;
const behind = this.behind;
const aheadBehindCounts = await this.view.container.git.getAheadBehindCommitCount(this.repoPath, [
createRevisionRange(behind.ref1 || 'HEAD', behind.ref2, '...'),
]);
const aheadBehindCounts = await this.view.container.git.getAheadBehindCommitCount(
this.repoPath,
[createRevisionRange(behind.ref1 || 'HEAD', behind.ref2, '...')],
{ authors: this.filterByAuthors },
);
const mergeBase =
(await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2, {
forkPoint: true,
@ -151,17 +172,23 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
expand: false,
},
),
new ResultsFilesNode(
this.view,
this,
this.repoPath,
this._compareWith.ref,
this._ref.ref,
this.getFilesQuery.bind(this),
undefined,
{ expand: false },
),
];
// Can't support showing files when commits are filtered
if (!this.filterByAuthors?.length) {
this._children.push(
new ResultsFilesNode(
this.view,
this,
this.repoPath,
this._compareWith.ref,
this._ref.ref,
this.getFilesQuery.bind(this),
undefined,
{ expand: false },
),
);
}
}
return this._children;
}
@ -183,7 +210,9 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
TreeItemCollapsibleState.Collapsed,
);
item.id = this.id;
item.contextValue = `${ContextValues.CompareResults}${this._ref.ref === '' ? '+working' : ''}`;
item.contextValue = `${ContextValues.CompareResults}${this._ref.ref === '' ? '+working' : ''}${
this.filterByAuthors?.length ? '+filtered' : ''
}`;
item.description = description;
item.iconPath = new ThemeIcon('compare-changes');
@ -299,6 +328,7 @@ export class CompareResultsNode extends SubscribeableViewNode<'compare-results',
const log = await this.view.container.git.getLog(repoPath, {
limit: limit,
ref: range,
authors: this.filterByAuthors,
});
const results: Mutable<Partial<CommitsQueryResults>> = {

+ 6
- 1
src/views/nodes/resultsCommitsNode.ts View File

@ -77,6 +77,10 @@ export class ResultsCommitsNode
return this._results.comparison?.ref2;
}
private get isComparisonFiltered(): boolean | undefined {
return this.context.comparisonFiltered;
}
private _onChildrenCompleted: Deferred<void> | undefined;
async getChildren(): Promise<ViewNode[]> {
@ -96,7 +100,8 @@ export class ResultsCommitsNode
this._expandAutolinks = false;
const { files } = this._results;
if (files != null) {
// Can't support showing files when commits are filtered
if (files != null && !this.isComparisonFiltered) {
children.push(
new ResultsFilesNode(
this.view,

+ 40
- 19
src/views/nodes/viewNode.ts View File

@ -40,6 +40,8 @@ import type { BranchNode } from './branchNode';
import type { BranchTrackingStatus } from './branchTrackingStatusNode';
import type { CommitFileNode } from './commitFileNode';
import type { CommitNode } from './commitNode';
import type { CompareBranchNode } from './compareBranchNode';
import type { CompareResultsNode } from './compareResultsNode';
import type { FileRevisionAsCommitNode } from './fileRevisionAsCommitNode';
import type { FolderNode } from './folderNode';
import type { LineHistoryTrackerNode } from './lineHistoryTrackerNode';
@ -125,6 +127,7 @@ export interface AmbientContext {
readonly branchStatusUpstreamType?: 'ahead' | 'behind' | 'same' | 'none';
readonly commit?: GitCommit;
readonly comparisonId?: string;
readonly comparisonFiltered?: boolean;
readonly contributor?: GitContributor;
readonly file?: GitFile;
readonly reflog?: GitReflogRecord;
@ -397,7 +400,8 @@ export function isPageableViewNode(node: ViewNode): node is ViewNode & PageableV
export abstract class SubscribeableViewNode<
Type extends TreeViewSubscribableNodeTypes = TreeViewSubscribableNodeTypes,
TView extends View = View,
> extends ViewNode<Type, TView> {
State extends object = any,
> extends ViewNode<Type, TView, State> {
protected disposable: Disposable;
protected subscription: Promise<Disposable | undefined> | undefined;
@ -831,24 +835,41 @@ export function getNodeRepoPath(node?: ViewNode): string | undefined {
}
type TreeViewNodesByType = {
[T in TreeViewNodeTypes]: ViewNode<T>;
} & {
['branch']: BranchNode;
['commit']: CommitNode;
['commit-file']: CommitFileNode;
['conflict-file']: MergeConflictFileNode;
['file-commit']: FileRevisionAsCommitNode;
['folder']: FolderNode;
['line-history-tracker']: LineHistoryTrackerNode;
['repository']: RepositoryNode;
['repo-folder']: RepositoryFolderNode;
['results-file']: ResultsFileNode;
['stash']: StashNode;
['stash-file']: StashFileNode;
['status-file']: StatusFileNode;
['tag']: TagNode;
['uncommitted-file']: UncommittedFileNode;
// Add more real types as needed
[T in TreeViewNodeTypes]: T extends 'branch'
? BranchNode
: T extends 'commit'
? CommitNode
: T extends 'commit-file'
? CommitFileNode
: T extends 'compare-branch'
? CompareBranchNode
: T extends 'compare-results'
? CompareResultsNode
: T extends 'conflict-file'
? MergeConflictFileNode
: T extends 'file-commit'
? FileRevisionAsCommitNode
: T extends 'folder'
? FolderNode
: T extends 'line-history-tracker'
? LineHistoryTrackerNode
: T extends 'repository'
? RepositoryNode
: T extends 'repo-folder'
? RepositoryFolderNode
: T extends 'results-file'
? ResultsFileNode
: T extends 'stash'
? StashNode
: T extends 'stash-file'
? StashFileNode
: T extends 'status-file'
? StatusFileNode
: T extends 'tag'
? TagNode
: T extends 'uncommitted-file'
? UncommittedFileNode
: ViewNode<T>;
};
export function isViewNode(node: unknown): node is ViewNode;

+ 57
- 0
src/views/viewCommands.ts View File

@ -19,8 +19,10 @@ import * as TagActions from '../git/actions/tag';
import * as WorktreeActions from '../git/actions/worktree';
import { GitUri } from '../git/gitUri';
import { deletedOrMissing } from '../git/models/constants';
import { matchContributor } from '../git/models/contributor';
import type { GitStashReference } from '../git/models/reference';
import { createReference, getReferenceLabel, shortenRevision } from '../git/models/reference';
import { showContributorsPicker } from '../quickpicks/contributorsPicker';
import {
executeActionCommand,
executeCommand,
@ -297,7 +299,19 @@ export class ViewCommands {
n => this.openWorktree(n, { location: 'newWindow' }),
this,
);
registerViewCommand(
'gitlens.views.setResultsCommitsFilterAuthors',
n => this.setResultsCommitsFilter(n, true),
this,
);
registerViewCommand(
'gitlens.views.setResultsCommitsFilterOff',
n => this.setResultsCommitsFilter(n, false),
this,
);
}
@debug()
private addAuthors(node?: ViewNode) {
return ContributorActions.addAuthors(getNodeRepoPath(node));
@ -1316,4 +1330,47 @@ export class ViewCommands {
return CommitActions.openFilesAtRevision(node.commit);
}
@debug()
private async setResultsCommitsFilter(node: ViewNode, filter: boolean) {
if (!node?.is('compare-results') && !node?.is('compare-branch')) return;
const repo = this.container.git.getRepository(node.repoPath);
if (repo == null) return;
if (filter) {
let authors = node.getState('filterCommits');
if (authors == null) {
const current = await this.container.git.getCurrentUser(repo.uri);
authors = current != null ? [current] : undefined;
}
const result = await showContributorsPicker(
this.container,
repo,
'Filter Commits',
repo.virtual ? 'Choose a contributor to show commits from' : 'Choose contributors to show commits from',
{
appendReposToTitle: true,
clearButton: true,
multiselect: !repo.virtual,
picked: c => authors?.some(u => matchContributor(c, u)) ?? false,
},
);
if (result == null) return;
if (result.length === 0) {
filter = false;
node.deleteState('filterCommits');
} else {
node.storeState('filterCommits', result);
}
} else if (repo != null) {
node.deleteState('filterCommits');
} else {
node.deleteState('filterCommits');
}
void node.triggerChange(true);
}
}

Loading…
Cancel
Save