From e56850a8d6cf3554bbf7a88ce0281dff2b4ce4cc Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 25 Aug 2022 02:28:54 -0400 Subject: [PATCH] Changes to provider specific graph data querying - Allows for more performant querying Optimizes local git graph data to avoid extra lookups --- src/env/node/git/git.ts | 4 +- src/env/node/git/localGitProvider.ts | 214 +++++++++++++++- src/git/gitProvider.ts | 11 + src/git/gitProviderService.ts | 16 ++ src/git/models/commit.ts | 4 + src/git/models/graph.ts | 34 +++ src/git/models/log.ts | 6 +- src/git/models/remote.ts | 18 ++ src/git/parsers/logParser.ts | 27 +- src/plus/github/githubGitProvider.ts | 168 ++++++++++++- src/plus/webviews/graph/graphWebview.ts | 349 +++++--------------------- src/plus/webviews/graph/protocol.ts | 18 +- src/system/utils.ts | 12 +- src/webviews/apps/plus/graph/GraphWrapper.tsx | 8 +- src/webviews/apps/plus/graph/graph.tsx | 10 +- 15 files changed, 575 insertions(+), 324 deletions(-) create mode 100644 src/git/models/graph.ts diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index d9cc8f7..93ce13a 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -712,7 +712,7 @@ export class Git { }, ) { if (argsOrFormat == null) { - argsOrFormat = ['--name-status', `--format=${GitLogParser.defaultFormat}`]; + argsOrFormat = ['--name-status', `--format=${all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`]; } if (typeof argsOrFormat === 'string') { @@ -808,7 +808,7 @@ export class Git { const [file, root] = splitPath(fileName, repoPath, true); if (argsOrFormat == null) { - argsOrFormat = [`--format=${GitLogParser.defaultFormat}`]; + argsOrFormat = [`--format=${all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`]; } if (typeof argsOrFormat === 'string') { diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index f9ec04b..a7dbb41 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -15,6 +15,7 @@ import type { import { configuration } from '../../../configuration'; import { CoreGitConfiguration, GlyphChars, Schemes } from '../../../constants'; import type { Container } from '../../../container'; +import { emojify } from '../../../emojis'; import { Features } from '../../../features'; import { StashApplyError, @@ -42,20 +43,34 @@ import { GitProviderService } from '../../../git/gitProviderService'; import { encodeGitLensRevisionUriAuthority, GitUri } from '../../../git/gitUri'; import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../../git/models/blame'; import type { BranchSortOptions } from '../../../git/models/branch'; -import { GitBranch, isDetachedHead, sortBranches } from '../../../git/models/branch'; +import { + getBranchNameWithoutRemote, + getRemoteNameFromBranchName, + GitBranch, + isDetachedHead, + sortBranches, +} from '../../../git/models/branch'; import type { GitStashCommit } from '../../../git/models/commit'; -import { GitCommit, GitCommitIdentity } from '../../../git/models/commit'; +import { GitCommit, GitCommitIdentity, isStash } from '../../../git/models/commit'; import { GitContributor } from '../../../git/models/contributor'; import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from '../../../git/models/diff'; import type { GitFile, GitFileStatus } from '../../../git/models/file'; import { GitFileChange } from '../../../git/models/file'; +import type { + GitGraph, + GitGraphRow, + GitGraphRowHead, + GitGraphRowRemoteHead, + GitGraphRowTag, +} from '../../../git/models/graph'; +import { GitGraphRowType } from '../../../git/models/graph'; import type { GitLog } from '../../../git/models/log'; import type { GitMergeStatus } from '../../../git/models/merge'; import type { GitRebaseStatus } from '../../../git/models/rebase'; import type { GitBranchReference } from '../../../git/models/reference'; import { GitReference, GitRevision } from '../../../git/models/reference'; import type { GitReflog } from '../../../git/models/reflog'; -import { GitRemote } from '../../../git/models/remote'; +import { getRemoteIconUri, GitRemote } from '../../../git/models/remote'; import type { RepositoryChangeEvent } from '../../../git/models/repository'; import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository'; import type { GitStash } from '../../../git/models/stash'; @@ -1584,6 +1599,179 @@ export class LocalGitProvider implements GitProvider, Disposable { } @log() + async getCommitsForGraph( + repoPath: string, + asWebviewUri: (uri: Uri) => Uri, + options?: { + branch?: string; + limit?: number; + mode?: 'single' | 'local' | 'all'; + ref?: string; + }, + ): Promise { + const [logResult, stashResult, remotesResult] = await Promise.allSettled([ + this.getLog(repoPath, { all: true, ordering: 'date', limit: options?.limit }), + this.getStash(repoPath), + this.getRemotes(repoPath), + ]); + + return this.getCommitsForGraphCore( + repoPath, + asWebviewUri, + getSettledValue(logResult), + getSettledValue(stashResult), + getSettledValue(remotesResult), + options, + ); + } + + private async getCommitsForGraphCore( + repoPath: string, + asWebviewUri: (uri: Uri) => Uri, + log: GitLog | undefined, + stash: GitStash | undefined, + remotes: GitRemote[] | undefined, + options?: { + ref?: string; + mode?: 'single' | 'local' | 'all'; + branch?: string; + }, + ): Promise { + if (log == null) { + return { + repoPath: repoPath, + rows: [], + }; + } + + const commits = (log.pagedCommits?.() ?? log.commits)?.values(); + if (commits == null) { + return { + repoPath: repoPath, + rows: [], + }; + } + + const rows: GitGraphRow[] = []; + + let current = false; + let refHeads: GitGraphRowHead[]; + let refRemoteHeads: GitGraphRowRemoteHead[]; + let refTags: GitGraphRowTag[]; + let parents: string[]; + let remoteName: string; + let isStashCommit: boolean; + + const remoteMap = remotes != null ? new Map(remotes.map(r => [r.name, r])) : new Map(); + + const skipStashParents = new Set(); + + for (const commit of commits) { + if (skipStashParents.has(commit.sha)) continue; + + refHeads = []; + refRemoteHeads = []; + refTags = []; + + if (commit.tips != null) { + for (let tip of commit.tips) { + if (tip === 'refs/stash' || tip === 'HEAD') continue; + + if (tip.startsWith('tag: ')) { + refTags.push({ + name: tip.substring(5), + // Not currently used, so don't bother filling it out + annotated: false, + }); + + continue; + } + + current = tip.startsWith('HEAD -> '); + if (current) { + tip = tip.substring(8); + } + + remoteName = getRemoteNameFromBranchName(tip); + if (remoteName) { + const remote = remoteMap.get(remoteName); + if (remote != null) { + const branchName = getBranchNameWithoutRemote(tip); + if (branchName === 'HEAD') continue; + + refRemoteHeads.push({ + name: branchName, + owner: remote.name, + url: remote.url, + avatarUrl: ( + remote.provider?.avatarUri ?? getRemoteIconUri(this.container, remote, asWebviewUri) + )?.toString(true), + }); + + continue; + } + } + + refHeads.push({ + name: tip, + isCurrentHead: current, + }); + } + } + + isStashCommit = isStash(commit) || (stash?.commits.has(commit.sha) ?? false); + + parents = commit.parents; + // Remove the second & third parent, if exists, from each stash commit as it is a Git implementation for the index and untracked files + if (isStashCommit && parents.length > 1) { + // Copy the array to avoid mutating the original + parents = [...parents]; + + // Skip the "index commit" (e.g. contains staged files) of the stash + skipStashParents.add(parents[1]); + // Skip the "untracked commit" (e.g. contains untracked files) of the stash + skipStashParents.add(parents[2]); + parents.splice(1, 2); + } + + rows.push({ + sha: commit.sha, + parents: parents, + author: commit.author.name, + avatarUrl: !isStashCommit ? (await commit.getAvatarUri())?.toString(true) : undefined, + email: commit.author.email ?? '', + date: commit.committer.date.getTime(), + message: emojify(commit.message && String(commit.message).length ? commit.message : commit.summary), + // TODO: review logic for stash, wip, etc + type: isStashCommit + ? GitGraphRowType.Stash + : commit.parents.length > 1 + ? GitGraphRowType.MergeCommit + : GitGraphRowType.Commit, + heads: refHeads, + remotes: refRemoteHeads, + tags: refTags, + }); + } + + return { + repoPath: repoPath, + paging: { + limit: log.limit, + endingCursor: log.endingCursor, + startingCursor: log.startingCursor, + more: log.hasMore, + }, + rows: rows, + + more: async (limit: number | { until: string } | undefined): Promise => { + const moreLog = await log.more?.(limit); + return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, stash, remotes, options); + }, + }; + } + + @log() async getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise { const [relativePath, root] = splitPath(uri, repoPath); @@ -2242,6 +2430,8 @@ export class LocalGitProvider implements GitProvider, Disposable { count: commits.size, limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, hasMore: moreUntil == null ? moreLog.hasMore : true, + startingCursor: last(log.commits)?.[0], + endingCursor: moreLog.endingCursor, pagedCommits: () => { // Remove any duplicates for (const sha of log.commits.keys()) { @@ -2249,7 +2439,6 @@ export class LocalGitProvider implements GitProvider, Disposable { } return moreLog.commits; }, - previousCursor: last(log.commits)?.[0], query: (limit: number | undefined) => this.getLog(log.repoPath, { ...options, limit: limit }), }; mergedLog.more = this.getLogMoreFn(mergedLog, options); @@ -3201,7 +3390,13 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getIncomingActivity( repoPath: string, - options?: { all?: boolean; branch?: string; limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number }, + options?: { + all?: boolean; + branch?: string; + limit?: number; + ordering?: 'date' | 'author-date' | 'topo' | null; + skip?: number; + }, ): Promise { const scope = getLogScope(); @@ -3229,7 +3424,13 @@ export class LocalGitProvider implements GitProvider, Disposable { private getReflogMoreFn( reflog: GitReflog, - options?: { all?: boolean; branch?: string; limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number }, + options?: { + all?: boolean; + branch?: string; + limit?: number; + ordering?: 'date' | 'author-date' | 'topo' | null; + skip?: number; + }, ): (limit: number) => Promise { return async (limit: number | undefined) => { limit = limit ?? configuration.get('advanced.maxSearchItems') ?? 0; @@ -3358,6 +3559,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ) ?? [], undefined, [], + undefined, s.stashName, onRef, ) as GitStashCommit, diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 2b54bba..549a01d 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -9,6 +9,7 @@ import type { GitCommit } from './models/commit'; import type { GitContributor } from './models/contributor'; import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from './models/diff'; import type { GitFile } from './models/file'; +import type { GitGraph } from './models/graph'; import type { GitLog } from './models/log'; import type { GitMergeStatus } from './models/merge'; import type { GitRebaseStatus } from './models/rebase'; @@ -217,6 +218,16 @@ export interface GitProvider extends Disposable { range?: Range | undefined; }, ): Promise; + getCommitsForGraph( + repoPath: string, + asWebviewUri: (uri: Uri) => Uri, + options?: { + branch?: string; + limit?: number; + mode?: 'single' | 'local' | 'all'; + ref?: string; + }, + ): Promise; getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise; getContributors( repoPath: string, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 98dc571..c059988 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -49,6 +49,7 @@ import type { GitCommit } from './models/commit'; import type { GitContributor } from './models/contributor'; import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from './models/diff'; import type { GitFile } from './models/file'; +import type { GitGraph } from './models/graph'; import type { GitLog } from './models/log'; import type { GitMergeStatus } from './models/merge'; import type { PullRequest, PullRequestState } from './models/pullRequest'; @@ -1313,6 +1314,21 @@ export class GitProviderService implements Disposable { } @log() + getCommitsForGraph( + repoPath: string | Uri, + asWebviewUri: (uri: Uri) => Uri, + options?: { + branch?: string; + limit?: number; + mode?: 'single' | 'local' | 'all'; + ref?: string; + }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getCommitsForGraph(path, asWebviewUri, options); + } + + @log() async getOldestUnpushedRefForFile(repoPath: string | Uri, uri: Uri): Promise { const { provider, path } = this.getProvider(repoPath); return provider.getOldestUnpushedRefForFile(path, uri); diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 6751ad8..f8cb0db 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -34,6 +34,7 @@ export class GitCommit implements GitRevisionReference { // TODO@eamodio rename to stashNumber readonly number: string | undefined; readonly stashOnRef: string | undefined; + readonly tips: string[] | undefined; constructor( private readonly container: Container, @@ -47,11 +48,13 @@ export class GitCommit implements GitRevisionReference { files?: GitFileChange | GitFileChange[] | { file?: GitFileChange; files?: GitFileChange[] } | undefined, stats?: GitCommitStats, lines?: GitCommitLine | GitCommitLine[] | undefined, + tips?: string[], stashName?: string | undefined, stashOnRef?: string | undefined, ) { this.ref = this.sha; this.shortSha = this.sha.substring(0, this.container.CommitShaFormatting.length); + this.tips = tips; if (stashName) { this.refType = 'stash'; @@ -562,6 +565,7 @@ export class GitCommit implements GitRevisionReference { files, this.stats, this.getChangedValue(changes.lines, this.lines), + this.tips, this.stashName, this.stashOnRef, ); diff --git a/src/git/models/graph.ts b/src/git/models/graph.ts new file mode 100644 index 0000000..8c70362 --- /dev/null +++ b/src/git/models/graph.ts @@ -0,0 +1,34 @@ +import type { GraphRow, Head, Remote, Tag } from '@gitkraken/gitkraken-components'; + +export type GitGraphRowHead = Head; +export type GitGraphRowRemoteHead = Remote; +export type GitGraphRowTag = Tag; +export const enum GitGraphRowType { + Commit = 'commit-node', + MergeCommit = 'merge-node', + Stash = 'stash-node', + Working = 'work-dir-changes', + Conflict = 'merge-conflict-node', + Rebase = 'unsupported-rebase-warning-node', +} + +export interface GitGraphRow extends GraphRow { + type: GitGraphRowType; + heads?: GitGraphRowHead[]; + remotes?: GitGraphRowRemoteHead[]; + tags?: GitGraphRowTag[]; +} + +export interface GitGraph { + readonly repoPath: string; + readonly rows: GitGraphRow[]; + + readonly paging?: { + readonly limit: number | undefined; + readonly startingCursor: string | undefined; + readonly endingCursor: string | undefined; + readonly more: boolean; + }; + + more?(limit: number | { until?: string } | undefined): Promise; +} diff --git a/src/git/models/log.ts b/src/git/models/log.ts index cbb6006..199ee90 100644 --- a/src/git/models/log.ts +++ b/src/git/models/log.ts @@ -4,17 +4,17 @@ import type { GitCommit } from './commit'; export interface GitLog { readonly repoPath: string; readonly commits: Map; + readonly count: number; readonly sha: string | undefined; readonly range: Range | undefined; - readonly count: number; readonly limit: number | undefined; + readonly startingCursor?: string; + readonly endingCursor?: string; readonly hasMore: boolean; - readonly cursor?: string; readonly pagedCommits?: () => Map; - readonly previousCursor?: string; query?(limit: number | undefined): Promise; more?(limit: number | { until?: string } | undefined): Promise; diff --git a/src/git/models/remote.ts b/src/git/models/remote.ts index a037e0c..8811f88 100644 --- a/src/git/models/remote.ts +++ b/src/git/models/remote.ts @@ -1,5 +1,8 @@ +import type { ColorTheme } from 'vscode'; +import { Uri, window } from 'vscode'; import { Container } from '../../container'; import { sortCompare } from '../../system/string'; +import { isLightTheme } from '../../system/utils'; import type { RemoteProvider } from '../remotes/provider'; import { RichRemoteProvider } from '../remotes/provider'; @@ -87,3 +90,18 @@ export class GitRemote Uri, + theme: ColorTheme = window.activeColorTheme, +): Uri | undefined { + if (remote.provider?.icon == null) return undefined; + + const uri = Uri.joinPath( + container.context.extensionUri, + `images/${isLightTheme(theme) ? 'light' : 'dark'}/icon-${remote.provider.icon}.svg`, + ); + return asWebviewUri != null ? asWebviewUri(uri) : uri; +} diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index ced58d7..10a17bc 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -61,6 +61,7 @@ interface LogEntry { fileStats?: GitFileChangeStats; summary?: string; + tips?: string[]; line?: GitCommitLine; } @@ -110,6 +111,23 @@ export class GitLogParser { return this._defaultParser; } + static allFormat = [ + `${lb}${sl}f${rb}`, + `${lb}r${rb}${sp}%H`, // ref + `${lb}a${rb}${sp}%aN`, // author + `${lb}e${rb}${sp}%aE`, // author email + `${lb}d${rb}${sp}%at`, // author date + `${lb}n${rb}${sp}%cN`, // committer + `${lb}m${rb}${sp}%cE`, // committer email + `${lb}c${rb}${sp}%ct`, // committer date + `${lb}p${rb}${sp}%P`, // parents + `${lb}t${rb}${sp}%D`, // tips + `${lb}s${rb}`, + '%B', // summary + `${lb}${sl}s${rb}`, + `${lb}f${rb}`, + ].join('%n'); + static defaultFormat = [ `${lb}${sl}f${rb}`, `${lb}r${rb}${sp}%H`, // ref @@ -354,7 +372,13 @@ export class GitLogParser { break; case 112: // 'p': // parents - entry.parentShas = line.substring(4).split(' '); + line = line.substring(4); + entry.parentShas = line.length !== 0 ? line.split(' ') : undefined; + break; + + case 116: // 't': // tips + line = line.substring(4); + entry.tips = line.length !== 0 ? line.split(', ') : undefined; break; case 115: // 's': // summary @@ -631,6 +655,7 @@ export class GitLogParser { files, undefined, entry.line != null ? [entry.line] : [], + entry.tips, ); commits.set(entry.sha!, commit); diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 2377182..cbaded9 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -6,6 +6,7 @@ import { configuration } from '../../configuration'; import { CharCode, ContextKeys, Schemes } from '../../constants'; import type { Container } from '../../container'; import { setContext } from '../../context'; +import { emojify } from '../../emojis'; import { AuthenticationError, AuthenticationErrorReason, @@ -35,13 +36,21 @@ import { GitContributor } from '../../git/models/contributor'; import type { GitDiff, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from '../../git/models/diff'; import type { GitFile } from '../../git/models/file'; import { GitFileChange, GitFileIndexStatus } from '../../git/models/file'; +import type { + GitGraph, + GitGraphRow, + GitGraphRowHead, + GitGraphRowRemoteHead, + GitGraphRowTag, +} from '../../git/models/graph'; +import { GitGraphRowType } from '../../git/models/graph'; import type { GitLog } from '../../git/models/log'; import type { GitMergeStatus } from '../../git/models/merge'; import type { GitRebaseStatus } from '../../git/models/rebase'; import type { GitBranchReference, GitReference } from '../../git/models/reference'; import { GitRevision } from '../../git/models/reference'; import type { GitReflog } from '../../git/models/reflog'; -import { GitRemote, GitRemoteType } from '../../git/models/remote'; +import { getRemoteIconUri, GitRemote, GitRemoteType } from '../../git/models/remote'; import type { RepositoryChangeEvent } from '../../git/models/repository'; import { Repository } from '../../git/models/repository'; import type { GitStash } from '../../git/models/stash'; @@ -174,6 +183,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { // if (supported != null) return supported; switch (feature) { + case Features.Stashes: case Features.Worktrees: return false; default: @@ -1030,6 +1040,142 @@ export class GitHubGitProvider implements GitProvider, Disposable { } @log() + async getCommitsForGraph( + repoPath: string, + asWebviewUri: (uri: Uri) => Uri, + options?: { + branch?: string; + limit?: number; + mode?: 'single' | 'local' | 'all'; + ref?: string; + }, + ): Promise { + const [logResult, branchResult, remotesResult, tagsResult] = await Promise.allSettled([ + this.getLog(repoPath, { all: true, ordering: 'date', limit: options?.limit }), + this.getBranch(repoPath), + this.getRemotes(repoPath), + this.getTags(repoPath), + ]); + + return this.getCommitsForGraphCore( + repoPath, + asWebviewUri, + getSettledValue(logResult), + getSettledValue(branchResult), + getSettledValue(remotesResult)?.[0], + getSettledValue(tagsResult)?.values, + options, + ); + } + + private async getCommitsForGraphCore( + repoPath: string, + asWebviewUri: (uri: Uri) => Uri, + log: GitLog | undefined, + branch: GitBranch | undefined, + remote: GitRemote | undefined, + tags: GitTag[] | undefined, + options?: { + ref?: string; + mode?: 'single' | 'local' | 'all'; + branch?: string; + }, + ): Promise { + if (log == null) { + return { + repoPath: repoPath, + rows: [], + }; + } + + const commits = (log.pagedCommits?.() ?? log.commits)?.values(); + if (commits == null) { + return { + repoPath: repoPath, + rows: [], + }; + } + + const rows: GitGraphRow[] = []; + + let refHeads: GitGraphRowHead[]; + let refRemoteHeads: GitGraphRowRemoteHead[]; + let refTags: GitGraphRowTag[]; + + const hasHeadShaAndRemote = branch?.sha != null && remote != null; + + for (const commit of commits) { + if (hasHeadShaAndRemote && commit.sha === branch.sha) { + refHeads = [ + { + name: branch.name, + isCurrentHead: true, + }, + ]; + refRemoteHeads = [ + { + name: branch.name, + owner: remote.name, + url: remote.url, + avatarUrl: ( + remote.provider?.avatarUri ?? getRemoteIconUri(this.container, remote, asWebviewUri) + )?.toString(true), + }, + ]; + } else { + refHeads = []; + refRemoteHeads = []; + } + + if (tags != null) { + refTags = [ + ...filterMap(tags, t => { + if (t.sha !== commit.sha) return undefined; + + return { + name: t.name, + annotated: Boolean(t.message), + }; + }), + ]; + } else { + refTags = []; + } + + rows.push({ + sha: commit.sha, + parents: commit.parents, + author: commit.author.name, + avatarUrl: (await commit.getAvatarUri())?.toString(true), + email: commit.author.email ?? '', + date: commit.committer.date.getTime(), + message: emojify(commit.message && String(commit.message).length ? commit.message : commit.summary), + // TODO: review logic for stash, wip, etc + type: commit.parents.length > 1 ? GitGraphRowType.MergeCommit : GitGraphRowType.Commit, + heads: refHeads, + remotes: refRemoteHeads, + tags: refTags, + }); + } + + return { + repoPath: repoPath, + paging: { + limit: log.limit, + endingCursor: log.endingCursor, + startingCursor: log.startingCursor, + more: log.hasMore, + }, + rows: rows, + + more: async (limit: number | { until: string } | undefined): Promise => { + const moreLog = await log.more?.(limit); + return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, branch, remote, tags, options); + }, + }; + } + + @log() async getOldestUnpushedRefForFile(_repoPath: string, _uri: Uri): Promise { // TODO@eamodio until we have access to the RemoteHub change store there isn't anything we can do here return undefined; @@ -1262,7 +1408,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: limit, hasMore: result.paging?.more ?? false, - cursor: result.paging?.cursor, + endingCursor: result.paging?.cursor, query: (limit: number | undefined) => this.getLog(repoPath, { ...options, limit: limit }), }; @@ -1342,7 +1488,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { const moreLog = await this.getLog(log.repoPath, { ...options, limit: moreLimit, - cursor: log.cursor, + cursor: log.endingCursor, }); // If we can't find any more, assume we have everything if (moreLog == null) return { ...log, hasMore: false }; @@ -1357,7 +1503,8 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, hasMore: moreUntil == null ? moreLog.hasMore : true, - cursor: moreLog.cursor, + startingCursor: last(log.commits)?.[0], + endingCursor: moreLog.endingCursor, pagedCommits: () => { // Remove any duplicates for (const sha of log.commits.keys()) { @@ -1365,7 +1512,6 @@ export class GitHubGitProvider implements GitProvider, Disposable { } return moreLog.commits; }, - previousCursor: last(log.commits)?.[0], query: log.query, }; mergedLog.more = this.getLogMoreFn(mergedLog, options); @@ -1510,7 +1656,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: limit, hasMore: result.pageInfo?.hasNextPage ?? false, - cursor: result.pageInfo?.endCursor ?? undefined, + endingCursor: result.pageInfo?.endCursor ?? undefined, query: (limit: number | undefined) => this.getLog(repoPath, { ...options, limit: limit }), }; @@ -1539,7 +1685,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { const moreLog = await this.getLogForSearch(log.repoPath, search, { ...options, limit: limit, - cursor: log.cursor, + cursor: log.endingCursor, }); // If we can't find any more, assume we have everything if (moreLog == null) return { ...log, hasMore: false }; @@ -1554,7 +1700,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: (log.limit ?? 0) + limit, hasMore: moreLog.hasMore, - cursor: moreLog.cursor, + endingCursor: moreLog.endingCursor, query: log.query, }; mergedLog.more = this.getLogForSearchMoreFn(mergedLog, search, options); @@ -1835,7 +1981,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: limit, hasMore: result.paging?.more ?? false, - cursor: result.paging?.cursor, + endingCursor: result.paging?.cursor, query: (limit: number | undefined) => this.getLogForFile(repoPath, path, { ...options, limit: limit }), }; @@ -1891,7 +2037,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { const moreLog = await this.getLogForFile(log.repoPath, relativePath, { ...options, limit: moreUntil == null ? moreLimit : 0, - cursor: log.cursor, + cursor: log.endingCursor, // ref: options.all ? undefined : moreUntil == null ? `${ref}^` : `${moreUntil}^..${ref}^`, // skip: options.all ? log.count : undefined, }); @@ -1908,7 +2054,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { count: commits.size, limit: moreUntil == null ? (log.limit ?? 0) + moreLimit : undefined, hasMore: moreUntil == null ? moreLog.hasMore : true, - cursor: moreLog.cursor, + endingCursor: moreLog.endingCursor, query: log.query, }; diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 3c69c22..710e1ea 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -1,35 +1,29 @@ -import type { CommitType, GraphRow, Head, Remote, Tag } from '@gitkraken/gitkraken-components'; -import { commitNodeType, mergeNodeType, stashNodeType } from '@gitkraken/gitkraken-components'; import type { ColorTheme, ConfigurationChangeEvent, Disposable, Event, StatusBarItem } from 'vscode'; -import { ColorThemeKind, EventEmitter, MarkdownString, StatusBarAlignment, Uri, ViewColumn, window } from 'vscode'; +import { EventEmitter, MarkdownString, StatusBarAlignment, ViewColumn, window } from 'vscode'; import { parseCommandContext } from '../../../commands/base'; import { GitActions } from '../../../commands/gitCommands.actions'; import type { GraphColumnConfig } from '../../../configuration'; import { configuration } from '../../../configuration'; -import { Commands } from '../../../constants'; +import { Commands, ContextKeys } from '../../../constants'; import type { Container } from '../../../container'; -import { emojify } from '../../../emojis'; -import type { GitBranch } from '../../../git/models/branch'; -import type { GitCommit, GitStashCommit } from '../../../git/models/commit'; -import { isStash } from '../../../git/models/commit'; -import type { GitLog } from '../../../git/models/log'; -import type { GitRemote } from '../../../git/models/remote'; +import { setContext } from '../../../context'; +import type { GitCommit } from '../../../git/models/commit'; +import type { GitGraph } from '../../../git/models/graph'; import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository'; import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository'; -import type { GitStash } from '../../../git/models/stash'; -import type { GitTag } from '../../../git/models/tag'; +import { registerCommand } from '../../../system/command'; +import { gate } from '../../../system/decorators/gate'; import { debug } from '../../../system/decorators/log'; import type { Deferrable } from '../../../system/function'; import { debounce } from '../../../system/function'; -import { filter, filterMap, union } from '../../../system/iterable'; import { updateRecordValue } from '../../../system/object'; -import { getSettledValue } from '../../../system/promise'; +import { isDarkTheme, isLightTheme } from '../../../system/utils'; import { RepositoryFolderNode } from '../../../views/nodes/viewNode'; import type { IpcMessage } from '../../../webviews/protocol'; import { onIpc } from '../../../webviews/protocol'; import { WebviewBase } from '../../../webviews/webviewBase'; import { ensurePlusFeaturesEnabled } from '../../subscription/utils'; -import type { GraphCompositeConfig, GraphLog, GraphRepository, State } from './protocol'; +import type { GraphCompositeConfig, GraphRepository, State } from './protocol'; import { DidChangeCommitsNotificationType, DidChangeGraphConfigurationNotificationType, @@ -61,8 +55,7 @@ export class GraphWebview extends WebviewBase { this._repositoryEventsDisposable?.dispose(); this._repository = value; - this._etagRepository = value?.etag; - this._repositoryLog = undefined; + this.resetRepositoryState(); if (value != null) { this._repositoryEventsDisposable = value.onDidChange(this.onRepositoryChanged, this); @@ -78,7 +71,8 @@ export class GraphWebview extends WebviewBase { private _etagRepository?: number; private _repositoryEventsDisposable: Disposable | undefined; - private _repositoryLog?: GitLog; + private _repositoryGraph?: GitGraph; + private _statusBarItem: StatusBarItem | undefined; private _theme: ColorTheme | undefined; @@ -121,13 +115,19 @@ export class GraphWebview extends WebviewBase { } if (this.repository != null) { - void this.refresh(); + this.resetRepositoryState(); + this.updateState(); } } return super.show(column, ...args); } + protected override refresh(force?: boolean): Promise { + this.resetRepositoryState(); + return super.refresh(force); + } + protected override async includeBootstrap(): Promise { return this.getState(); } @@ -178,8 +178,7 @@ export class GraphWebview extends WebviewBase { protected override onVisibilityChanged(visible: boolean): void { if (visible && this.repository != null && this.repository.etag !== this._etagRepository) { - this._repositoryLog = undefined; - void this.refresh(); + this.updateState(true); } } @@ -209,10 +208,6 @@ export class GraphWebview extends WebviewBase { this._statusBarItem = undefined; } } - - if (e != null && configuration.changed(e, 'graph')) { - this.updateState(); - } } private onRepositoryChanged(e: RepositoryChangeEvent) { @@ -220,9 +215,9 @@ export class GraphWebview extends WebviewBase { !e.changed( RepositoryChange.Config, RepositoryChange.Heads, - RepositoryChange.Index, + // RepositoryChange.Index, RepositoryChange.Remotes, - RepositoryChange.RemoteProviders, + // RepositoryChange.RemoteProviders, RepositoryChange.Stash, RepositoryChange.Status, RepositoryChange.Tags, @@ -230,10 +225,10 @@ export class GraphWebview extends WebviewBase { RepositoryChangeComparisonMode.Any, ) ) { + this._etagRepository = e.repository.etag; return; } - this._repositoryLog = undefined; this.updateState(); } @@ -267,21 +262,27 @@ export class GraphWebview extends WebviewBase { void this.notifyDidChangeGraphConfiguration(); } + @gate() private async onGetMoreCommits(limit?: number) { - if (this._repositoryLog?.more != null) { - const { defaultItemLimit, pageItemLimit } = this.getConfig(); - const nextLog = await this._repositoryLog.more(limit ?? pageItemLimit ?? defaultItemLimit); - if (nextLog != null) { - this._repositoryLog = nextLog; - } + if (this._repositoryGraph?.more == null || this._repository?.etag !== this._etagRepository) { + this.updateState(true); + + return; } + + const { defaultItemLimit, pageItemLimit } = this.getConfig(); + const newGraph = await this._repositoryGraph.more(limit ?? pageItemLimit ?? defaultItemLimit); + if (newGraph != null) { + this._repositoryGraph = newGraph; + } else { + debugger; + } + void this.notifyDidChangeCommits(); } private onRepositorySelectionChanged(path: string) { - if (this.repository?.path !== path) { - this.repository = this.container.git.getRepository(path); - } + this.repository = this.container.git.getRepository(path); } private async onSelectionChanged(selection: string[]) { @@ -343,95 +344,17 @@ export class GraphWebview extends WebviewBase { private async notifyDidChangeCommits() { if (!this.isReady || !this.visible) return false; - const data = await this.getGraphData(true); + const data = this._repositoryGraph!; return this.notify(DidChangeCommitsNotificationType, { rows: data.rows, - log: formatLog(data.log), - previousCursor: data.log?.previousCursor, + paging: { + startingCursor: data.paging?.startingCursor, + endingCursor: data.paging?.endingCursor, + more: data.paging?.more ?? false, + }, }); } - private async getGraphData(paging: boolean = false): Promise<{ log: GitLog | undefined; rows: GraphRow[] }> { - const [logResult, stashResult, branchesResult, tagsResult, remotesResult] = await Promise.allSettled([ - this.getLog(), - this.getStash(), - this.getBranches(), - this.getTags(), - this.getRemotes(), - ]); - - const log = getSettledValue(logResult); - const combinedCommits = combineLogAndStash(log, getSettledValue(stashResult), paging); - - const rows = await convertToRows( - combinedCommits, - getSettledValue(branchesResult) ?? [], - getSettledValue(tagsResult) ?? [], - getSettledValue(remotesResult) ?? [], - icon => - this._panel?.webview - .asWebviewUri( - Uri.joinPath( - this.container.context.extensionUri, - `images/${isLightTheme(window.activeColorTheme) ? 'light' : 'dark'}/icon-${icon}.svg`, - ), - ) - .toString(), - ); - - return { - log: log, - rows: rows, - }; - } - - private async getLog(): Promise { - if (this.repository == null) return undefined; - - if (this._repositoryLog == null) { - const { defaultItemLimit, pageItemLimit } = this.getConfig(); - const log = await this.container.git.getLog(this.repository.uri, { - all: true, - ordering: 'date', - limit: defaultItemLimit ?? pageItemLimit, - }); - if (log?.commits == null) return undefined; - - this._repositoryLog = log; - } - - if (this._repositoryLog?.commits == null) return undefined; - - return this._repositoryLog; - } - - private async getBranches(): Promise { - const branches = await this.repository?.getBranches(); - if (branches?.paging?.more) { - debugger; - // TODO@eamodio - implement paging - } - return branches?.values; - } - - private async getTags(): Promise { - const tags = await this.repository?.getTags(); - if (tags?.paging?.more) { - debugger; - // TODO@eamodio - implement paging - } - return tags?.values; - } - - private async getRemotes(): Promise { - return this.repository?.getRemotes(); - } - - private async getStash(): Promise { - // TODO@eamodio look into using `git log -g stash` to get stashes with the commits - return this.repository?.getStash(); - } - private getConfig(): GraphCompositeConfig { const settings = configuration.get('graph'); const config: GraphCompositeConfig = { @@ -451,157 +374,42 @@ export class GraphWebview extends WebviewBase { if (this.repository == null) { this.repository = this.container.git.getBestRepositoryOrFirst(); + if (this.repository == null) return { repositories: [] }; } - if (this.repository != null) { - this.title = `${this.originalTitle}: ${this.repository.formattedName}`; - } - const data = await this.getGraphData(false); + this._etagRepository = this.repository?.etag; + this.title = `${this.originalTitle}: ${this.repository.formattedName}`; + + const config = this.getConfig(); + + // If we have a set of data refresh to the same set + const limit = this._repositoryGraph?.paging?.limit ?? config.defaultItemLimit; + + const data = await this.container.git.getCommitsForGraph( + this.repository.path, + this._panel!.webview.asWebviewUri, + { limit: limit }, + ); + this._repositoryGraph = data; return { previewBanner: this.previewBanner, repositories: formatRepositories(this.container.git.openRepositories), - selectedRepository: this.repository?.path, + selectedRepository: this.repository.path, rows: data.rows, - log: formatLog(data.log), - config: this.getConfig(), + paging: { + startingCursor: data.paging?.startingCursor, + endingCursor: data.paging?.endingCursor, + more: data.paging?.more ?? false, + }, + config: config, nonce: this.cspNonce, }; } -} - -function combineLogAndStash( - log: GitLog | undefined, - stash: GitStash | undefined, - paging = false, -): Iterable { - // let commits = log?.commits; - // if (commits == null) return []; - - // if (paging && log?.previousCursor != null) { - // let pagedCommits = [...commits.values()]; - // const index = pagedCommits.findIndex(c => c.sha === log?.previousCursor); - // if (index !== -1) { - // pagedCommits = pagedCommits.slice(index + 1); - // } else { - // debugger; - // } - - // commits = new Map(pagedCommits.map(c => [c.sha, c])); - // } - - const commits = (paging ? log?.pagedCommits?.() : undefined) ?? log?.commits; - if (commits == null) return []; - if (stash?.commits == null) return [...commits.values()]; - - const stashCommitShaSecondParents = new Set( - filterMap(stash.commits.values(), c => (c.parents.length > 1 ? c.parents[1] : undefined)), - ); - const filteredCommits = filter( - commits.values(), - c => !stash.commits.has(c.sha) && !stashCommitShaSecondParents.has(c.sha), - ); - - const filteredStashCommits = filter(stash.commits.values(), c => !c.parents?.length || commits.has(c.parents[0])); - - return union(filteredCommits, filteredStashCommits); -} -async function convertToRows( - commits: Iterable, - branches: GitBranch[], - tags: GitTag[], - remotes: GitRemote[], - getRemoteIconUrl: (icon?: string) => string | undefined, -): Promise { - const rows: GraphRow[] = []; - - let graphHeads: Head[]; - let graphTags: Tag[]; - let graphRemotes: Remote[]; - let parents: string[]; - let stash: boolean; - - const remoteMap = new Map(remotes.map(r => [r.name, r])); - - for (const commit of commits) { - graphHeads = [ - ...filterMap(branches, b => { - if (b.sha !== commit.sha || b.remote) return undefined; - - return { - name: b.name, - isCurrentHead: b.current, - }; - }), - ]; - - graphRemotes = [ - ...filterMap(branches, b => { - if (b.sha !== commit.sha || !b.remote) return undefined; - - const remoteName = b.getRemoteName(); - const remote = remoteName != null ? remoteMap.get(remoteName) : undefined; - - return { - name: b.getNameWithoutRemote(), - url: remote?.url, - avatarUrl: - remote?.provider?.avatarUri?.toString(true) ?? - (remote?.provider?.icon != null ? getRemoteIconUrl(remote.provider.icon) : undefined), - owner: remote?.name, - }; - }), - ]; - - graphTags = [ - ...filterMap(tags, t => { - if (t.sha !== commit.sha) return undefined; - - return { - name: t.name, - annotated: Boolean(t.message), - }; - }), - ]; - - stash = isStash(commit); - - parents = commit.parents; - // Remove the second parent, if existing, from each stash commit as it affects column processing - if (stash && parents.length > 1) { - // Copy the array to avoid mutating the original - parents = [...parents]; - parents.splice(1, 1); - } - - rows.push({ - sha: commit.sha, - parents: parents, - author: commit.author.name, - avatarUrl: !stash ? (await commit.getAvatarUri())?.toString(true) : undefined, - email: commit.author.email ?? '', - date: commit.committer.date.getTime(), - message: emojify(commit.message && String(commit.message).length ? commit.message : commit.summary), - type: getCommitType(commit), // TODO: review logic for stash, wip, etc - heads: graphHeads, - remotes: graphRemotes, - tags: graphTags, - }); + private resetRepositoryState() { + this._repositoryGraph = undefined; } - - return rows; -} - -function formatLog(log: GitLog | undefined): GraphLog | undefined { - if (log == null) return undefined; - - return { - count: log.count, - limit: log.limit, - hasMore: log.hasMore, - cursor: log.cursor, - }; } function formatRepositories(repositories: Repository[]): GraphRepository[] { @@ -614,24 +422,3 @@ function formatRepositories(repositories: Repository[]): GraphRepository[] { path: r.path, })); } - -function getCommitType(commit: GitCommit | GitStashCommit): CommitType { - if (isStash(commit)) { - return stashNodeType as CommitType; - } - - if (commit.parents.length > 1) { - return mergeNodeType as CommitType; - } - - // TODO: add other needed commit types for graph - return commitNodeType as CommitType; -} - -function isDarkTheme(theme: ColorTheme): boolean { - return theme.kind === ColorThemeKind.Dark || theme.kind === ColorThemeKind.HighContrast; -} - -function isLightTheme(theme: ColorTheme): boolean { - return theme.kind === ColorThemeKind.Light || theme.kind === ColorThemeKind.HighContrastLight; -} diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index f329c9b..9f6605b 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -6,18 +6,19 @@ export interface State { repositories?: GraphRepository[]; selectedRepository?: string; rows?: GraphRow[]; + paging?: GraphPaging; config?: GraphCompositeConfig; - log?: GraphLog; nonce?: string; - mixedColumnColors?: Record; previewBanner?: boolean; + + // Props below are computed in the webview (not passed) + mixedColumnColors?: Record; } -export interface GraphLog { - count: number; - limit?: number; - hasMore: boolean; - cursor?: string; +export interface GraphPaging { + startingCursor?: string; + endingCursor?: string; + more: boolean; } export interface GraphRepository { @@ -95,8 +96,7 @@ export const DidChangeGraphConfigurationNotificationType = new IpcNotificationTy export interface DidChangeCommitsParams { rows: GraphRow[]; - previousCursor?: string; - log?: GraphLog; + paging?: GraphPaging; } export const DidChangeCommitsNotificationType = new IpcNotificationType( 'graph/commits/didChange', diff --git a/src/system/utils.ts b/src/system/utils.ts index d0bd55d..c8b443b 100644 --- a/src/system/utils.ts +++ b/src/system/utils.ts @@ -1,5 +1,5 @@ -import type { TextDocument, TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; -import { ViewColumn, window, workspace } from 'vscode'; +import type { ColorTheme, TextDocument, TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; +import { ColorThemeKind, ViewColumn, window, workspace } from 'vscode'; import { configuration } from '../configuration'; import { CoreCommands, ImageMimetypes, Schemes } from '../constants'; import { isGitUri } from '../git/gitUri'; @@ -77,6 +77,14 @@ export function isActiveDocument(document: TextDocument): boolean { return editor != null && editor.document === document; } +export function isDarkTheme(theme: ColorTheme): boolean { + return theme.kind === ColorThemeKind.Dark || theme.kind === ColorThemeKind.HighContrast; +} + +export function isLightTheme(theme: ColorTheme): boolean { + return theme.kind === ColorThemeKind.Light || theme.kind === ColorThemeKind.HighContrastLight; +} + export function isVirtualUri(uri: Uri): boolean { return uri.scheme === Schemes.Virtual || uri.scheme === Schemes.GitHub; } diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 4bea5fa..c0d4419 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -115,7 +115,7 @@ export function GraphWrapper({ rows = [], selectedRepository, config, - log, + paging, onSelectRepository, onColumnChange, onMoreCommits, @@ -131,7 +131,7 @@ export function GraphWrapper({ reposList.find(item => item.path === selectedRepository), ); const [graphColSettings, setGraphColSettings] = useState(getGraphColSettingsModel(config)); - const [logState, setLogState] = useState(log); + const [pagingState, setPagingState] = useState(paging); const [isLoading, setIsLoading] = useState(false); const [styleProps, setStyleProps] = useState(getStyleProps(mixedColumnColors)); // TODO: application shouldn't know about the graph component's header @@ -170,7 +170,7 @@ export function GraphWrapper({ setReposList(state.repositories ?? []); setCurrentRepository(reposList.find(item => item.path === state.selectedRepository)); setGraphColSettings(getGraphColSettingsModel(state.config)); - setLogState(state.log); + setPagingState(state.paging); setIsLoading(false); setStyleProps(getStyleProps(state.mixedColumnColors)); } @@ -248,7 +248,7 @@ export function GraphWrapper({ getExternalIcon={getIconElementLibrary} graphRows={graphList} height={mainHeight} - hasMoreCommits={logState?.hasMore} + hasMoreCommits={pagingState?.more} isLoadingRows={isLoading} nonce={nonce} onColumnResized={handleOnColumnResized} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 8342e7c..fc67cf3 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -44,7 +44,7 @@ export class GraphApp extends App { protected override onBind() { const disposables = super.onBind?.() ?? []; - this.log('GraphApp onBind log', this.state.log); + this.log('GraphApp.onBind paging:', this.state.paging); const $root = document.getElementById('root'); if ($root != null) { @@ -93,7 +93,7 @@ export class GraphApp extends App { onIpc(DidChangeCommitsNotificationType, msg, params => { let rows; - if (params?.previousCursor != null && this.state.rows != null) { + if (params?.paging?.startingCursor != null && this.state.rows != null) { const previousRows = this.state.rows; const lastSha = previousRows[previousRows.length - 1]?.sha; @@ -104,12 +104,12 @@ export class GraphApp extends App { // Preallocate the array to avoid reallocations rows.length = previousRowsLength + newRowsLength; - if (params.previousCursor !== lastSha) { + if (params.paging.startingCursor !== lastSha) { let i = 0; let row; for (row of previousRows) { rows[i++] = row; - if (row.sha === params.previousCursor) { + if (row.sha === params.paging.startingCursor) { previousRowsLength = i; if (previousRowsLength !== previousRows.length) { @@ -136,7 +136,7 @@ export class GraphApp extends App { this.setState({ ...this.state, rows: rows, - log: params.log, + paging: params.paging, }); this.refresh(this.state); });