Browse Source

Improves view perf when connected to rich remote

- Showing and refreshing the _Commits_ view
  - Expanding commits, branches, and worktrees
main
Eric Amodio 2 years ago
parent
commit
b8683921cd
10 changed files with 439 additions and 176 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +14
    -4
      src/git/models/branch.ts
  3. +8
    -2
      src/git/models/commit.ts
  4. +130
    -78
      src/views/nodes/branchNode.ts
  5. +122
    -34
      src/views/nodes/commitNode.ts
  6. +13
    -4
      src/views/nodes/fileRevisionAsCommitNode.ts
  7. +6
    -2
      src/views/nodes/rebaseStatusNode.ts
  8. +35
    -3
      src/views/nodes/viewNode.ts
  9. +94
    -46
      src/views/nodes/worktreeNode.ts
  10. +14
    -3
      src/views/viewBase.ts

+ 3
- 0
CHANGELOG.md View File

@ -25,6 +25,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Changed
- Greatly improves performance of many view interactions when connected to a rich integration and pull request details are enabled, including:
- Showing and refreshing the _Commits_ view
- Expanding commits, branches, and worktrees
- Remembers chosen filter on files nodes in comparisons when refreshing
- Changes display of filtered state of files nodes in comparisons
- Improves diff stat parsing performance and reduced memory usage

+ 14
- 4
src/git/models/branch.ts View File

@ -4,6 +4,7 @@ import { Starred, WorkspaceStorageKeys } from '../../storage';
import { formatDate, fromNow } from '../../system/date';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { cancellable } from '../../system/promise';
import { sortCompare } from '../../system/string';
import { PullRequest, PullRequestState } from './pullRequest';
import { GitBranchReference, GitReference, GitRevision } from './reference';
@ -154,6 +155,8 @@ export class GitBranch implements GitBranchReference {
return this.date != null ? fromNow(this.date) : '';
}
private _pullRequest: Promise<PullRequest | undefined> | undefined;
@debug()
async getAssociatedPullRequest(options?: {
avatarSize?: number;
@ -161,11 +164,18 @@ export class GitBranch implements GitBranchReference {
limit?: number;
timeout?: number;
}): Promise<PullRequest | undefined> {
const remote = await this.getRemote();
if (remote == null) return undefined;
if (this._pullRequest == null) {
async function getCore(this: GitBranch): Promise<PullRequest | undefined> {
const remote = await this.getRemote();
if (remote == null) return undefined;
const branch = this.getTrackingWithoutRemote() ?? this.getNameWithoutRemote();
return Container.instance.git.getPullRequestForBranch(branch, remote, options);
}
this._pullRequest = getCore.call(this);
}
const branch = this.getTrackingWithoutRemote() ?? this.getNameWithoutRemote();
return Container.instance.git.getPullRequestForBranch(branch, remote, options);
return cancellable(this._pullRequest, options?.timeout);
}
@memoize()

+ 8
- 2
src/git/models/commit.ts View File

@ -10,9 +10,11 @@ import { cancellable } from '../../system/promise';
import { pad, pluralize } from '../../system/string';
import { PreviousLineComparisonUrisResult } from '../gitProvider';
import { GitUri } from '../gitUri';
import { RichRemoteProvider } from '../remotes/provider';
import { GitFile, GitFileChange, GitFileWorkingTreeStatus } from './file';
import { PullRequest } from './pullRequest';
import { GitReference, GitRevision, GitRevisionReference, GitStashReference } from './reference';
import { GitRemote } from './remote';
import { Repository } from './repository';
const stashNumberRegex = /stash@{(\d+)}/;
@ -389,10 +391,14 @@ export class GitCommit implements GitRevisionReference {
}
private _pullRequest: Promise<PullRequest | undefined> | undefined;
async getAssociatedPullRequest(options?: { timeout?: number }): Promise<PullRequest | undefined> {
async getAssociatedPullRequest(options?: {
remote?: GitRemote<RichRemoteProvider>;
timeout?: number;
}): Promise<PullRequest | undefined> {
if (this._pullRequest == null) {
async function getCore(this: GitCommit): Promise<PullRequest | undefined> {
const remote = await this.container.git.getBestRemoteWithRichProvider(this.repoPath);
const remote =
options?.remote ?? (await this.container.git.getBestRemoteWithRichProvider(this.repoPath));
if (remote?.provider == null) return undefined;
return this.container.git.getPullRequestForCommit(this.ref, remote, options);

+ 130
- 78
src/views/nodes/branchNode.ts View File

@ -10,11 +10,13 @@ import {
GitRemote,
GitRemoteType,
GitUser,
PullRequest,
PullRequestState,
} from '../../git/models';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { map } from '../../system/iterable';
import { getSettledValue } from '../../system/promise';
import { pad } from '../../system/string';
import { BranchesView } from '../branchesView';
import { CommitsView } from '../commitsView';
@ -31,8 +33,13 @@ import { RebaseStatusNode } from './rebaseStatusNode';
import { RepositoryNode } from './repositoryNode';
import { ContextValues, PageableViewNode, ViewNode, ViewRefNode } from './viewNode';
type State = {
pullRequest: PullRequest | null | undefined;
pendingPullRequest: Promise<PullRequest | undefined> | undefined;
};
export class BranchNode
extends ViewRefNode<BranchesView | CommitsView | RemotesView | RepositoriesView, GitBranchReference>
extends ViewRefNode<BranchesView | CommitsView | RemotesView | RepositoriesView, GitBranchReference, State>
implements PageableViewNode
{
static key = ':branch';
@ -40,7 +47,6 @@ export class BranchNode
return `${RepositoryNode.getId(repoPath)}${this.key}(${name})${root ? ':root' : ''}`;
}
private _children: ViewNode[] | undefined;
private readonly options: {
expanded: boolean;
limitCommits: boolean;
@ -129,67 +135,103 @@ export class BranchNode
: this.branch.getNameWithoutRemote().split('/');
}
private _children: ViewNode[] | undefined;
async getChildren(): Promise<ViewNode[]> {
if (this._children == null) {
let prPromise;
const branch = this.branch;
const pullRequest = this.getState('pullRequest');
if (
this.view.config.pullRequests.enabled &&
this.view.config.pullRequests.showForBranches &&
(this.branch.upstream != null || this.branch.remote)
(branch.upstream != null || branch.remote)
) {
prPromise = this.branch.getAssociatedPullRequest(
this.root ? { include: [PullRequestState.Open, PullRequestState.Merged] } : undefined,
);
if (pullRequest === undefined && this.getState('pendingPullRequest') === undefined) {
void this.getAssociatedPullRequest(
branch,
this.root ? { include: [PullRequestState.Open, PullRequestState.Merged] } : undefined,
).then(pr => {
// If we found a pull request, insert it into the children cache (if loaded) and refresh the node
if (pr != null && this._children != null) {
this._children.splice(
this._children[0] instanceof CompareBranchNode ? 1 : 0,
0,
new PullRequestNode(this.view, this, pr, branch),
);
}
this.view.triggerNodeChange(this);
});
// If we are showing the node, then refresh this node to show a spinner while the pull request is loading
if (!this.splatted) {
queueMicrotask(() => this.view.triggerNodeChange(this));
return [];
}
}
}
const range = !this.branch.remote
? await this.view.container.git.getBranchAheadRange(this.branch)
: undefined;
const [log, getBranchAndTagTips, status, mergeStatus, rebaseStatus, unpublishedCommits] = await Promise.all(
[
this.getLog(),
this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name),
this.options.showStatus && this.branch.current
? this.view.container.git.getStatusForRepo(this.uri.repoPath)
: undefined,
this.options.showStatus && this.branch.current
? this.view.container.git.getMergeStatus(this.uri.repoPath!)
: undefined,
this.options.showStatus ? this.view.container.git.getRebaseStatus(this.uri.repoPath!) : undefined,
range
? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, {
limit: 0,
ref: range,
})
: undefined,
],
);
const [
logResult,
getBranchAndTagTipsResult,
statusResult,
mergeStatusResult,
rebaseStatusResult,
unpublishedCommitsResult,
] = await Promise.allSettled([
this.getLog(),
this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, branch.name),
this.options.showStatus && branch.current
? this.view.container.git.getStatusForRepo(this.uri.repoPath)
: undefined,
this.options.showStatus && branch.current
? this.view.container.git.getMergeStatus(this.uri.repoPath!)
: undefined,
this.options.showStatus ? this.view.container.git.getRebaseStatus(this.uri.repoPath!) : undefined,
!branch.remote
? this.view.container.git.getBranchAheadRange(branch).then(range =>
range
? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, {
limit: 0,
ref: range,
})
: undefined,
)
: undefined,
]);
const log = getSettledValue(logResult);
if (log == null) return [new MessageNode(this.view, this, 'No commits could be found.')];
const children = [];
let prInsertIndex = 0;
if (this.options.showComparison !== false && !(this.view instanceof RemotesView)) {
prInsertIndex++;
children.push(
new CompareBranchNode(
this.uri,
this.view,
this,
this.branch,
branch,
this.options.showComparison,
this.splatted,
),
);
}
if (pullRequest != null) {
children.push(new PullRequestNode(this.view, this, pullRequest, branch));
}
const status = getSettledValue(statusResult);
const mergeStatus = getSettledValue(mergeStatusResult);
const rebaseStatus = getSettledValue(rebaseStatusResult);
if (this.options.showStatus && mergeStatus != null) {
children.push(
new MergeStatusNode(
this.view,
this,
this.branch,
branch,
mergeStatus,
status ?? (await this.view.container.git.getStatusForRepo(this.uri.repoPath)),
this.root,
@ -198,13 +240,13 @@ export class BranchNode
} else if (
this.options.showStatus &&
rebaseStatus != null &&
(this.branch.current || this.branch.name === rebaseStatus.incoming.name)
(branch.current || branch.name === rebaseStatus.incoming.name)
) {
children.push(
new RebaseStatusNode(
this.view,
this,
this.branch,
branch,
rebaseStatus,
status ?? (await this.view.container.git.getStatusForRepo(this.uri.repoPath)),
this.root,
@ -212,34 +254,30 @@ export class BranchNode
);
} else if (this.options.showTracking) {
const status = {
ref: this.branch.ref,
repoPath: this.branch.repoPath,
state: this.branch.state,
upstream: this.branch.upstream?.name,
ref: branch.ref,
repoPath: branch.repoPath,
state: branch.state,
upstream: branch.upstream?.name,
};
if (this.branch.upstream != null) {
if (branch.upstream != null) {
if (this.root && !status.state.behind && !status.state.ahead) {
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'same', this.root),
);
children.push(new BranchTrackingStatusNode(this.view, this, branch, status, 'same', this.root));
} else {
if (status.state.behind) {
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'behind', this.root),
new BranchTrackingStatusNode(this.view, this, branch, status, 'behind', this.root),
);
}
if (status.state.ahead) {
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'ahead', this.root),
new BranchTrackingStatusNode(this.view, this, branch, status, 'ahead', this.root),
);
}
}
} else {
children.push(
new BranchTrackingStatusNode(this.view, this, this.branch, status, 'none', this.root),
);
children.push(new BranchTrackingStatusNode(this.view, this, branch, status, 'none', this.root));
}
}
@ -247,6 +285,9 @@ export class BranchNode
children.push(new MessageNode(this.view, this, '', GlyphChars.Dash.repeat(2), ''));
}
const unpublishedCommits = getSettledValue(unpublishedCommitsResult);
const getBranchAndTagTips = getSettledValue(getBranchAndTagTipsResult);
children.push(
...insertDateMarkers(
map(
@ -257,7 +298,7 @@ export class BranchNode
this,
c,
unpublishedCommits?.has(c.ref),
this.branch,
branch,
getBranchAndTagTips,
),
),
@ -268,34 +309,14 @@ export class BranchNode
if (log.hasMore) {
children.push(
new LoadMoreNode(this.view, this, children[children.length - 1], {
getCount: () => this.view.container.git.getCommitCount(this.branch.repoPath, this.branch.name),
getCount: () => this.view.container.git.getCommitCount(branch.repoPath, branch.name),
}),
);
}
if (prPromise != null) {
const pr = await prPromise;
if (pr != null) {
children.splice(prInsertIndex, 0, new PullRequestNode(this.view, this, pr, this.branch));
}
// const pr = await Promise.race([
// prPromise,
// new Promise<null>(resolve => setTimeout(() => resolve(null), 100)),
// ]);
// if (pr != null) {
// children.splice(prInsertIndex, 0, new PullRequestNode(this.view, this, pr, this.branch));
// } else if (pr === null) {
// void prPromise.then(pr => {
// if (pr == null) return;
// void this.triggerChange();
// });
// }
}
this._children = children;
}
return this._children;
}
@ -425,6 +446,11 @@ export class BranchNode
tooltip.appendMarkdown('\\\n$(star-full) Favorited');
}
const pendingPullRequest = this.getState('pendingPullRequest');
if (pendingPullRequest != null) {
tooltip.appendMarkdown(`\n\n$(loading~spin) Loading associated pull request${GlyphChars.Ellipsis}`);
}
const item = new TreeItem(
this.label,
this.options.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed,
@ -432,12 +458,15 @@ export class BranchNode
item.id = this.id;
item.contextValue = contextValue;
item.description = description;
item.iconPath = this.options.showAsCommits
? new ThemeIcon('git-commit', color)
: {
dark: this.view.container.context.asAbsolutePath(`images/dark/icon-branch${iconSuffix}.svg`),
light: this.view.container.context.asAbsolutePath(`images/light/icon-branch${iconSuffix}.svg`),
};
item.iconPath =
pendingPullRequest != null
? new ThemeIcon('loading~spin')
: this.options.showAsCommits
? new ThemeIcon('git-commit', color)
: {
dark: this.view.container.context.asAbsolutePath(`images/dark/icon-branch${iconSuffix}.svg`),
light: this.view.container.context.asAbsolutePath(`images/light/icon-branch${iconSuffix}.svg`),
};
item.tooltip = tooltip;
item.resourceUri = Uri.parse(
`gitlens-view://branch/status/${await this.branch.getStatus()}${
@ -466,7 +495,30 @@ export class BranchNode
this._children = undefined;
if (reset) {
this._log = undefined;
this.deleteState();
}
}
private async getAssociatedPullRequest(
branch: GitBranch,
options?: { include?: PullRequestState[] },
): Promise<PullRequest | undefined> {
let pullRequest = this.getState('pullRequest');
if (pullRequest !== undefined) return Promise.resolve(pullRequest ?? undefined);
let pendingPullRequest = this.getState('pendingPullRequest');
if (pendingPullRequest == null) {
pendingPullRequest = branch.getAssociatedPullRequest(options);
this.storeState('pendingPullRequest', pendingPullRequest);
pullRequest = await pendingPullRequest;
this.storeState('pullRequest', pullRequest ?? null);
this.deleteState('pendingPullRequest');
return pullRequest;
}
return pendingPullRequest;
}
private _log: GitLog | undefined;

+ 122
- 34
src/views/nodes/commitNode.ts View File

@ -3,9 +3,12 @@ import type { DiffWithPreviousCommandArgs } from '../../commands';
import { ViewFilesLayout } from '../../configuration';
import { Colors, Commands } from '../../constants';
import { CommitFormatter } from '../../git/formatters';
import { GitBranch, GitCommit, GitRevisionReference } from '../../git/models';
import { GitBranch, GitCommit, GitRemote, GitRevisionReference, PullRequest } from '../../git/models';
import { RichRemoteProvider } from '../../git/remotes/provider';
import { makeHierarchical } from '../../system/array';
import { gate } from '../../system/decorators/gate';
import { joinPaths, normalizePath } from '../../system/path';
import { getSettledValue } from '../../system/promise';
import { sortCompare } from '../../system/string';
import { FileHistoryView } from '../fileHistoryView';
import { TagsView } from '../tagsView';
@ -13,9 +16,20 @@ import { ViewsWithCommits } from '../viewBase';
import { CommitFileNode } from './commitFileNode';
import { FileNode, FolderNode } from './folderNode';
import { PullRequestNode } from './pullRequestNode';
import { RepositoryNode } from './repositoryNode';
import { ContextValues, ViewNode, ViewRefNode } from './viewNode';
export class CommitNode extends ViewRefNode<ViewsWithCommits | FileHistoryView, GitRevisionReference> {
type State = {
pullRequest: PullRequest | null | undefined;
pendingPullRequest: Promise<PullRequest | undefined> | undefined;
};
export class CommitNode extends ViewRefNode<ViewsWithCommits | FileHistoryView, GitRevisionReference, State> {
static key = ':commit';
static getId(repoPath: string, sha: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}(${sha})`;
}
constructor(
view: ViewsWithCommits | FileHistoryView,
parent: ViewNode,
@ -32,6 +46,10 @@ export class CommitNode extends ViewRefNode
return `${this.commit.shortSha}: ${this.commit.summary}`;
}
override get id(): string {
return `${this.parent?.id}|${CommitNode.getId(this.commit.repoPath, this.commit.sha)}`;
}
get isTip(): boolean {
return (this.branch?.current && this.branch.sha === this.commit.ref) ?? false;
}
@ -40,38 +58,69 @@ export class CommitNode extends ViewRefNode
return this.commit;
}
private _children: (PullRequestNode | FileNode)[] | undefined;
async getChildren(): Promise<ViewNode[]> {
const commit = this.commit;
if (this._children == null) {
const commit = this.commit;
const commits = await commit.getCommitsForFiles();
let children: (PullRequestNode | FileNode)[] = commits.map(
c => new CommitFileNode(this.view, this, c.file!, c),
);
const pullRequest = this.getState('pullRequest');
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = makeHierarchical(
children as FileNode[],
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy);
children = root.getChildren() as FileNode[];
} else {
(children as FileNode[]).sort((a, b) => sortCompare(a.label!, b.label!));
}
let children: (PullRequestNode | FileNode)[] = [];
if (!(this.view instanceof TagsView) && !(this.view instanceof FileHistoryView)) {
if (this.view.config.pullRequests.enabled && this.view.config.pullRequests.showForCommits) {
const pr = await commit.getAssociatedPullRequest();
if (pr != null) {
children.splice(0, 0, new PullRequestNode(this.view, this, pr, commit));
if (
!(this.view instanceof TagsView) &&
!(this.view instanceof FileHistoryView) &&
this.view.config.pullRequests.enabled &&
this.view.config.pullRequests.showForCommits
) {
if (pullRequest === undefined && this.getState('pendingPullRequest') === undefined) {
void this.getAssociatedPullRequest(commit).then(pr => {
// If we found a pull request, insert it into the children cache (if loaded) and refresh the node
if (pr != null && this._children != null) {
this._children.splice(
0,
0,
new PullRequestNode(this.view as ViewsWithCommits, this, pr, commit),
);
}
// Refresh this node to show a spinner while the pull request is loading
this.view.triggerNodeChange(this);
});
// Refresh this node to show a spinner while the pull request is loading
queueMicrotask(() => this.view.triggerNodeChange(this));
return [];
}
}
const commits = await commit.getCommitsForFiles();
for (const c of commits) {
children.push(new CommitFileNode(this.view, this, c.file!, c));
}
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = makeHierarchical(
children as FileNode[],
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy);
children = root.getChildren() as FileNode[];
} else {
(children as FileNode[]).sort((a, b) => sortCompare(a.label!, b.label!));
}
if (pullRequest != null) {
children.splice(0, 0, new PullRequestNode(this.view as ViewsWithCommits, this, pullRequest, commit));
}
this._children = children;
}
return children;
return this._children;
}
async getTreeItem(): Promise<TreeItem> {
@ -85,7 +134,7 @@ export class CommitNode extends ViewRefNode
label,
this._options.expand ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed,
);
item.id = this.id;
item.contextValue = `${ContextValues.Commit}${this.branch?.current ? '+current' : ''}${
this.isTip ? '+HEAD' : ''
}${this.unpublished ? '+unpublished' : ''}`;
@ -95,11 +144,17 @@ export class CommitNode extends ViewRefNode
getBranchAndTagTips: (sha: string) => this.getBranchAndTagTips?.(sha, { compact: true }),
messageTruncateAtNewLine: true,
});
item.iconPath = this.unpublished
? new ThemeIcon('arrow-up', new ThemeColor(Colors.UnpublishedCommitIconColor))
: this.view.config.avatars
? await this.commit.getAvatarUri({ defaultStyle: this.view.container.config.defaultGravatarsStyle })
: new ThemeIcon('git-commit');
const pendingPullRequest = this.getState('pendingPullRequest');
item.iconPath =
pendingPullRequest != null
? new ThemeIcon('loading~spin')
: this.unpublished
? new ThemeIcon('arrow-up', new ThemeColor(Colors.UnpublishedCommitIconColor))
: this.view.config.avatars
? await this.commit.getAvatarUri({ defaultStyle: this.view.container.config.defaultGravatarsStyle })
: new ThemeIcon('git-commit');
// item.tooltip = this.tooltip;
return item;
@ -122,6 +177,14 @@ export class CommitNode extends ViewRefNode
};
}
@gate()
override refresh(reset?: boolean) {
this._children = undefined;
if (reset) {
this.deleteState();
}
}
override async resolveTreeItem(item: TreeItem): Promise<TreeItem> {
if (item.tooltip == null) {
item.tooltip = await this.getTooltip();
@ -129,6 +192,28 @@ export class CommitNode extends ViewRefNode
return item;
}
private async getAssociatedPullRequest(
commit: GitCommit,
remote?: GitRemote<RichRemoteProvider>,
): Promise<PullRequest | undefined> {
let pullRequest = this.getState('pullRequest');
if (pullRequest !== undefined) return Promise.resolve(pullRequest ?? undefined);
let pendingPullRequest = this.getState('pendingPullRequest');
if (pendingPullRequest == null) {
pendingPullRequest = commit.getAssociatedPullRequest({ remote: remote });
this.storeState('pendingPullRequest', pendingPullRequest);
pullRequest = await pendingPullRequest;
this.storeState('pullRequest', pullRequest ?? null);
this.deleteState('pendingPullRequest');
return pullRequest;
}
return pendingPullRequest;
}
private async getTooltip() {
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath, { sort: true });
const remote = await this.view.container.git.getBestRemoteWithRichProvider(remotes);
@ -141,13 +226,16 @@ export class CommitNode extends ViewRefNode
let pr;
if (remote?.provider != null) {
[autolinkedIssuesOrPullRequests, pr] = await Promise.all([
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary,
remote,
),
this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider),
this.getAssociatedPullRequest(this.commit, remote),
]);
autolinkedIssuesOrPullRequests = getSettledValue(autolinkedIssuesOrPullRequestsResult);
pr = getSettledValue(prResult);
}
const tooltip = await CommitFormatter.fromTemplateAsync(

+ 13
- 4
src/views/nodes/fileRevisionAsCommitNode.ts View File

@ -14,6 +14,7 @@ import { CommitFormatter, StatusFileFormatter } from '../../git/formatters';
import { GitUri } from '../../git/gitUri';
import { GitBranch, GitCommit, GitFile, GitRevisionReference } from '../../git/models';
import { joinPaths } from '../../system/path';
import { getSettledValue } from '../../system/promise';
import { FileHistoryView } from '../fileHistoryView';
import { LineHistoryView } from '../lineHistoryView';
import { ViewsWithCommits } from '../viewBase';
@ -52,11 +53,16 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
async getChildren(): Promise<ViewNode[]> {
if (!this.commit.file?.hasConflicts) return [];
const [mergeStatus, rebaseStatus] = await Promise.all([
const [mergeStatusResult, rebaseStatusResult] = await Promise.allSettled([
this.view.container.git.getMergeStatus(this.commit.repoPath),
this.view.container.git.getRebaseStatus(this.commit.repoPath),
]);
if (mergeStatus == null && rebaseStatus == null) return [];
const mergeStatus = getSettledValue(mergeStatusResult);
if (mergeStatus == null) return [];
const rebaseStatus = getSettledValue(rebaseStatusResult);
if (rebaseStatus == null) return [];
return [
new MergeConflictCurrentChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file),
@ -208,13 +214,16 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
let pr;
if (remote?.provider != null) {
[autolinkedIssuesOrPullRequests, pr] = await Promise.all([
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary,
remote,
),
this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider),
this.commit.getAssociatedPullRequest({ remote: remote }),
]);
autolinkedIssuesOrPullRequests = getSettledValue(autolinkedIssuesOrPullRequestsResult);
pr = getSettledValue(prResult);
}
const status = StatusFileFormatter.fromTemplate(`\${status}\${ (originalPath)}`, this.file);

+ 6
- 2
src/views/nodes/rebaseStatusNode.ts View File

@ -8,6 +8,7 @@ import { GitBranch, GitCommit, GitRebaseStatus, GitReference, GitRevisionReferen
import { makeHierarchical } from '../../system/array';
import { executeCoreCommand } from '../../system/command';
import { joinPaths, normalizePath } from '../../system/path';
import { getSettledValue } from '../../system/promise';
import { pluralize, sortCompare } from '../../system/string';
import { ViewsWithCommits } from '../viewBase';
import { BranchNode } from './branchNode';
@ -199,13 +200,16 @@ export class RebaseCommitNode extends ViewRefNode
let pr;
if (remote?.provider != null) {
[autolinkedIssuesOrPullRequests, pr] = await Promise.all([
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary,
remote,
),
this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider),
this.commit.getAssociatedPullRequest({ remote: remote }),
]);
autolinkedIssuesOrPullRequests = getSettledValue(autolinkedIssuesOrPullRequestsResult);
pr = getSettledValue(prResult);
}
const tooltip = await CommitFormatter.fromTemplateAsync(

+ 35
- 3
src/views/nodes/viewNode.ts View File

@ -95,7 +95,7 @@ export interface ViewNode {
}
@logName<ViewNode>((c, name) => `${name}${c.id != null ? `(${c.id})` : ''}`)
export abstract class ViewNode<TView extends View = View> {
export abstract class ViewNode<TView extends View = View, State extends object = any> {
static is(node: any): node is ViewNode {
return node instanceof ViewNode;
}
@ -148,12 +148,40 @@ export abstract class ViewNode {
}
getSplattedChild?(): Promise<ViewNode | undefined>;
deleteState<T extends StateKey<State> = StateKey<State>>(key?: T): void {
if (this.id == null) {
debugger;
throw new Error('Id is required to delete state');
}
return this.view.nodeState.deleteState(this.id, key as string);
}
getState<T extends StateKey<State> = StateKey<State>>(key: T): StateValue<State, T> | undefined {
if (this.id == null) {
debugger;
throw new Error('Id is required to get state');
}
return this.view.nodeState.getState(this.id, key as string);
}
storeState<T extends StateKey<State> = StateKey<State>>(key: T, value: StateValue<State, T>): void {
if (this.id == null) {
debugger;
throw new Error('Id is required to store state');
}
this.view.nodeState.storeState(this.id, key as string, value);
}
}
type StateKey<T> = keyof T;
type StateValue<T, P extends StateKey<T>> = P extends keyof T ? T[P] : never;
export abstract class ViewRefNode<
TView extends View = View,
TReference extends GitReference = GitReference,
> extends ViewNode<TView> {
State extends object = any,
> extends ViewNode<TView, State> {
abstract get ref(): TReference;
get repoPath(): string {
@ -165,7 +193,11 @@ export abstract class ViewRefNode<
}
}
export abstract class ViewRefFileNode<TView extends View = View> extends ViewRefNode<TView, GitRevisionReference> {
export abstract class ViewRefFileNode<TView extends View = View, State extends object = any> extends ViewRefNode<
TView,
GitRevisionReference,
State
> {
abstract get file(): GitFile;
override toString(): string {

+ 94
- 46
src/views/nodes/worktreeNode.ts View File

@ -8,11 +8,13 @@ import {
GitRemoteType,
GitRevision,
GitWorktree,
PullRequest,
PullRequestState,
} from '../../git/models';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { map } from '../../system/iterable';
import { getSettledValue } from '../../system/promise';
import { pad } from '../../system/string';
import { RepositoriesView } from '../repositoriesView';
import { WorktreesView } from '../worktreesView';
@ -25,14 +27,18 @@ import { RepositoryNode } from './repositoryNode';
import { UncommittedFilesNode } from './UncommittedFilesNode';
import { ContextValues, ViewNode } from './viewNode';
export class WorktreeNode extends ViewNode<WorktreesView | RepositoriesView> {
type State = {
pullRequest: PullRequest | null | undefined;
pendingPullRequest: Promise<PullRequest | undefined> | undefined;
};
export class WorktreeNode extends ViewNode<WorktreesView | RepositoriesView, State> {
static key = ':worktree';
static getId(repoPath: string, uri: Uri): string {
return `${RepositoryNode.getId(repoPath)}${this.key}(${uri.path})`;
}
private _branch: GitBranch | undefined;
private _children: ViewNode[] | undefined;
constructor(
uri: GitUri,
@ -55,45 +61,65 @@ export class WorktreeNode extends ViewNode {
return this.uri.repoPath!;
}
private _children: ViewNode[] | undefined;
async getChildren(): Promise<ViewNode[]> {
if (this._children == null) {
const branch = this._branch;
let prPromise;
const pullRequest = this.getState('pullRequest');
if (
branch != null &&
this.view.config.pullRequests.enabled &&
this.view.config.pullRequests.showForBranches &&
(branch.upstream != null || branch.remote)
) {
prPromise = branch.getAssociatedPullRequest({
include: [PullRequestState.Open, PullRequestState.Merged],
});
if (pullRequest === undefined && this.getState('pendingPullRequest') === undefined) {
void this.getAssociatedPullRequest(branch, {
include: [PullRequestState.Open, PullRequestState.Merged],
}).then(pr => {
// If we found a pull request, insert it into the children cache (if loaded) and refresh the node
if (pr != null && this._children != null) {
this._children.splice(
this._children[0] instanceof CompareBranchNode ? 1 : 0,
0,
new PullRequestNode(this.view, this, pr, branch),
);
}
this.view.triggerNodeChange(this);
});
// If we are showing the node, then refresh this node to show a spinner while the pull request is loading
if (!this.splatted) {
queueMicrotask(() => this.view.triggerNodeChange(this));
return [];
}
}
}
const range =
branch != null && !branch.remote
? await this.view.container.git.getBranchAheadRange(branch)
: undefined;
const [log, getBranchAndTagTips, status, unpublishedCommits] = await Promise.all([
this.getLog(),
this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath),
this.worktree.getStatus(),
range
? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, {
limit: 0,
ref: range,
})
: undefined,
]);
const [logResult, getBranchAndTagTipsResult, statusResult, unpublishedCommitsResult] =
await Promise.allSettled([
this.getLog(),
this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath),
this.worktree.getStatus(),
branch != null && !branch.remote
? this.view.container.git.getBranchAheadRange(branch).then(range =>
range
? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, {
limit: 0,
ref: range,
})
: undefined,
)
: undefined,
]);
const log = getSettledValue(logResult);
if (log == null) return [new MessageNode(this.view, this, 'No commits could be found.')];
const children = [];
let prInsertIndex = 0;
if (branch != null && this.view.config.showBranchComparison !== false) {
prInsertIndex++;
children.push(
new CompareBranchNode(
this.uri,
@ -106,6 +132,13 @@ export class WorktreeNode extends ViewNode {
);
}
if (branch != null && pullRequest != null) {
children.push(new PullRequestNode(this.view, this, pullRequest, branch));
}
const unpublishedCommits = getSettledValue(unpublishedCommitsResult);
const getBranchAndTagTips = getSettledValue(getBranchAndTagTipsResult);
children.push(
...insertDateMarkers(
map(
@ -128,33 +161,15 @@ export class WorktreeNode extends ViewNode {
children.push(new LoadMoreNode(this.view, this, children[children.length - 1]));
}
const status = getSettledValue(statusResult);
if (status?.hasChanges) {
children.splice(0, 0, new UncommittedFilesNode(this.view, this, status, undefined));
}
if (prPromise != null) {
const pr = await prPromise;
if (pr != null) {
children.splice(prInsertIndex, 0, new PullRequestNode(this.view, this, pr, branch!));
}
// const pr = await Promise.race([
// prPromise,
// new Promise<null>(resolve => setTimeout(() => resolve(null), 100)),
// ]);
// if (pr != null) {
// children.splice(prInsertIndex, 0, new PullRequestNode(this.view, this, pr, this.branch));
// } else if (pr === null) {
// void prPromise.then(pr => {
// if (pr == null) return;
// void this.triggerChange();
// });
// }
}
this._children = children;
}
return this._children;
}
@ -300,13 +315,23 @@ export class WorktreeNode extends ViewNode {
}
}
const pendingPullRequest = this.getState('pendingPullRequest');
if (pendingPullRequest != null) {
tooltip.appendMarkdown(`\n\n$(loading~spin) Loading associated pull request${GlyphChars.Ellipsis}`);
}
const item = new TreeItem(this.worktree.name, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.description = description;
item.contextValue = `${ContextValues.Worktree}${this.worktree.main ? '+main' : ''}${
this.worktree.opened ? '+active' : ''
}`;
item.iconPath = this.worktree.opened ? new ThemeIcon('check') : icon;
item.iconPath =
pendingPullRequest != null
? new ThemeIcon('loading~spin')
: this.worktree.opened
? new ThemeIcon('check')
: icon;
item.tooltip = tooltip;
item.resourceUri = hasChanges ? Uri.parse('gitlens-view://worktree/changes') : undefined;
return item;
@ -318,9 +343,32 @@ export class WorktreeNode extends ViewNode {
this._children = undefined;
if (reset) {
this._log = undefined;
this.deleteState();
}
}
private async getAssociatedPullRequest(
branch: GitBranch,
options?: { include?: PullRequestState[] },
): Promise<PullRequest | undefined> {
let pullRequest = this.getState('pullRequest');
if (pullRequest !== undefined) return Promise.resolve(pullRequest ?? undefined);
let pendingPullRequest = this.getState('pendingPullRequest');
if (pendingPullRequest == null) {
pendingPullRequest = branch.getAssociatedPullRequest(options);
this.storeState('pendingPullRequest', pendingPullRequest);
pullRequest = await pendingPullRequest;
this.storeState('pullRequest', pullRequest ?? null);
this.deleteState('pendingPullRequest');
return pullRequest;
}
return pendingPullRequest;
}
private _log: GitLog | undefined;
private async getLog() {
if (this._log == null) {

+ 14
- 3
src/views/viewBase.ts View File

@ -166,6 +166,8 @@ export abstract class ViewBase<
}
dispose() {
this._nodeState?.dispose();
this._nodeState = undefined;
Disposable.from(...this.disposables).dispose();
}
@ -586,11 +588,20 @@ export abstract class ViewBase<
}
}
export class ViewNodeState {
export class ViewNodeState implements Disposable {
private _state: Map<string, Map<string, unknown>> | undefined;
deleteState(id: string, key: string): void {
this._state?.get(id)?.delete(key);
dispose() {
this._state?.clear();
this._state = undefined;
}
deleteState(id: string, key?: string): void {
if (key == null) {
this._state?.delete(id);
} else {
this._state?.get(id)?.delete(key);
}
}
getState<T>(id: string, key: string): T | undefined {

Loading…
Cancel
Save