Переглянути джерело

Changes to provider specific graph data querying

- Allows for more performant querying
Optimizes local git graph data to avoid extra lookups
main
Eric Amodio 2 роки тому
джерело
коміт
e56850a8d6
15 змінених файлів з 575 додано та 324 видалено
  1. +2
    -2
      src/env/node/git/git.ts
  2. +208
    -6
      src/env/node/git/localGitProvider.ts
  3. +11
    -0
      src/git/gitProvider.ts
  4. +16
    -0
      src/git/gitProviderService.ts
  5. +4
    -0
      src/git/models/commit.ts
  6. +34
    -0
      src/git/models/graph.ts
  7. +3
    -3
      src/git/models/log.ts
  8. +18
    -0
      src/git/models/remote.ts
  9. +26
    -1
      src/git/parsers/logParser.ts
  10. +157
    -11
      src/plus/github/githubGitProvider.ts
  11. +68
    -281
      src/plus/webviews/graph/graphWebview.ts
  12. +9
    -9
      src/plus/webviews/graph/protocol.ts
  13. +10
    -2
      src/system/utils.ts
  14. +4
    -4
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  15. +5
    -5
      src/webviews/apps/plus/graph/graph.tsx

+ 2
- 2
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') {

+ 208
- 6
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<GitGraph> {
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<GitGraph> {
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<GitGraph | undefined> => {
const moreLog = await log.more?.(limit);
return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, stash, remotes, options);
},
};
}
@log()
async getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise<string | undefined> {
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<GitReflog | undefined> {
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<GitReflog> {
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,

+ 11
- 0
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<GitCommit | undefined>;
getCommitsForGraph(
repoPath: string,
asWebviewUri: (uri: Uri) => Uri,
options?: {
branch?: string;
limit?: number;
mode?: 'single' | 'local' | 'all';
ref?: string;
},
): Promise<GitGraph>;
getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise<string | undefined>;
getContributors(
repoPath: string,

+ 16
- 0
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<GitGraph> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitsForGraph(path, asWebviewUri, options);
}
@log()
async getOldestUnpushedRefForFile(repoPath: string | Uri, uri: Uri): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getOldestUnpushedRefForFile(path, uri);

+ 4
- 0
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,
);

+ 34
- 0
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<GitGraph | undefined>;
}

+ 3
- 3
src/git/models/log.ts Переглянути файл

@ -4,17 +4,17 @@ import type { GitCommit } from './commit';
export interface GitLog {
readonly repoPath: string;
readonly commits: Map<string, GitCommit>;
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<string, GitCommit>;
readonly previousCursor?: string;
query?(limit: number | undefined): Promise<GitLog | undefined>;
more?(limit: number | { until?: string } | undefined): Promise<GitLog | undefined>;

+ 18
- 0
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
await repository?.setRemoteAsDefault(this, value);
}
}
export function getRemoteIconUri(
container: Container,
remote: GitRemote,
asWebviewUri?: (uri: Uri) => 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;
}

+ 26
- 1
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);

+ 157
- 11
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<GitGraph> {
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<GitGraph> {
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<GitGraph | undefined> => {
const moreLog = await log.more?.(limit);
return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, branch, remote, tags, options);
},
};
}
@log()
async getOldestUnpushedRefForFile(_repoPath: string, _uri: Uri): Promise<string | undefined> {
// 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,
};

+ 68
- 281
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<void> {
this.resetRepositoryState();
return super.refresh(force);
}
protected override async includeBootstrap(): Promise<State> {
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<GitLog | undefined> {
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<GitBranch[] | undefined> {
const branches = await this.repository?.getBranches();
if (branches?.paging?.more) {
debugger;
// TODO@eamodio - implement paging
}
return branches?.values;
}
private async getTags(): Promise<GitTag[] | undefined> {
const tags = await this.repository?.getTags();
if (tags?.paging?.more) {
debugger;
// TODO@eamodio - implement paging
}
return tags?.values;
}
private async getRemotes(): Promise<GitRemote[] | undefined> {
return this.repository?.getRemotes();
}
private async getStash(): Promise<GitStash | undefined> {
// 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<GitCommit | GitStashCommit> {
// 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<GitCommit>,
branches: GitBranch[],
tags: GitTag[],
remotes: GitRemote[],
getRemoteIconUrl: (icon?: string) => string | undefined,
): Promise<GraphRow[]> {
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;
}

+ 9
- 9
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<string, string>;
previewBanner?: boolean;
// Props below are computed in the webview (not passed)
mixedColumnColors?: Record<string, string>;
}
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<DidChangeCommitsParams>(
'graph/commits/didChange',

+ 10
- 2
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;
}

+ 4
- 4
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}

+ 5
- 5
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);
});

Завантаження…
Відмінити
Зберегти