From 1263b333ca4f852d2bbccad2c85281e9a8bf6684 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 8 Sep 2022 02:39:58 -0400 Subject: [PATCH] Optimizes graph log parsing & paging Adds graph.commitOrdering setting --- package.json | 29 +- src/config.ts | 1 + src/env/node/git/localGitProvider.ts | 456 +++++++++++++++----------------- src/git/parsers/logParser.ts | 370 +++++++++++++++----------- src/plus/webviews/graph/graphWebview.ts | 9 +- 5 files changed, 458 insertions(+), 407 deletions(-) diff --git a/package.json b/package.json index 6ff6efd..4df5b28 100644 --- a/package.json +++ b/package.json @@ -2092,10 +2092,20 @@ "title": "Commit Graph", "order": 105, "properties": { - "gitlens.graph.statusBar.enabled": { - "type": "boolean", - "default": true, - "markdownDescription": "Specifies whether to show the _Commit Graph_ in the status bar", + "gitlens.graph.commitOrdering": { + "type": "string", + "default": "date", + "enum": [ + "date", + "author-date", + "topo" + ], + "enumDescriptions": [ + "Shows commits in reverse chronological order of the commit timestamp", + "Shows commits in reverse chronological order of the author timestamp", + "Shows commits in reverse chronological order of the commit timestamp, but avoids intermixing multiple lines of history" + ], + "markdownDescription": "Specifies the order by which commits will be shown on the _Commit Graph_", "scope": "window", "order": 10 }, @@ -2104,14 +2114,21 @@ "default": 500, "markdownDescription": "Specifies the default number of items to show in the _Commit Graph_. Use 0 to specify no limit", "scope": "window", - "order": 50 + "order": 20 }, "gitlens.graph.pageItemLimit": { "type": "number", "default": 200, "markdownDescription": "Specifies the number of additional items to fetch when paginating in the _Commit Graph_. Use 0 to specify no limit", "scope": "window", - "order": 60 + "order": 21 + }, + "gitlens.graph.statusBar.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Specifies whether to show the _Commit Graph_ in the status bar", + "scope": "window", + "order": 100 } } }, diff --git a/src/config.ts b/src/config.ts index 018fbbd..4c47f9b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -378,6 +378,7 @@ export interface GraphColumnConfig { } export interface GraphConfig { + commitOrdering: 'date' | 'author-date' | 'topo'; defaultItemLimit: number; pageItemLimit: number; statusBar: { diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 5f4728a..9f2ac10 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -52,7 +52,7 @@ import { sortBranches, } from '../../../git/models/branch'; import type { GitStashCommit } from '../../../git/models/commit'; -import { GitCommit, GitCommitIdentity, isStash } from '../../../git/models/commit'; +import { GitCommit, GitCommitIdentity } 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'; @@ -87,7 +87,15 @@ import type { GitWorktree } from '../../../git/models/worktree'; import { GitBlameParser } from '../../../git/parsers/blameParser'; import { GitBranchParser } from '../../../git/parsers/branchParser'; import { GitDiffParser } from '../../../git/parsers/diffParser'; -import { GitLogParser, LogType } from '../../../git/parsers/logParser'; +import { + createLogParser, + createLogParserSingle, + createLogParserWithFiles, + getGraphParser, + getGraphRefParser, + GitLogParser, + LogType, +} from '../../../git/parsers/logParser'; import { GitReflogParser } from '../../../git/parsers/reflogParser'; import { GitRemoteParser } from '../../../git/parsers/remoteParser'; import { GitStatusParser } from '../../../git/parsers/statusParser'; @@ -1611,15 +1619,41 @@ export class LocalGitProvider implements GitProvider, Disposable { ref?: string; }, ): Promise { - const scope = getLogScope(); - - let stdin: string | undefined; - - const [stashResult, headResult] = await Promise.allSettled([ + const parser = getGraphParser(); + const refParser = getGraphRefParser(); + + const defaultLimit = options?.limit ?? configuration.get('graph.defaultItemLimit') ?? 5000; + const defaultPageLimit = configuration.get('graph.pageItemLimit') ?? 1000; + const ordering = configuration.get('graph.commitOrdering', undefined, 'date'); + + const [headResult, refResult, stashResult, remotesResult] = await Promise.allSettled([ + this.git.rev_parse(repoPath, 'HEAD'), + options?.ref != null && options?.ref !== 'HEAD' + ? this.git.log2(repoPath, options.ref, undefined, ...refParser.arguments, '-n1') + : undefined, this.getStash(repoPath), - options?.ref != null && options.ref !== 'HEAD' ? this.git.rev_parse(repoPath, 'HEAD') : undefined, + this.getRemotes(repoPath), ]); + let limit = defaultLimit; + let selectSha: string | undefined; + let since: string | undefined; + + const commit = first(refParser.parse(getSettledValue(refResult) ?? '')); + const head = getSettledValue(headResult); + if (commit != null && commit.sha !== head) { + since = ordering === 'author-date' ? commit.authorDate : commit.committerDate; + selectSha = commit.sha; + limit = 0; + } else if (options?.ref != null && (options.ref === 'HEAD' || options.ref === head)) { + selectSha = head; + } + + const remotes = getSettledValue(remotesResult); + const remoteMap = remotes != null ? new Map(remotes.map(r => [r.name, r])) : new Map(); + const skipStashParents = new Set(); + + let stdin: string | undefined; // TODO@eamodio this is insanity -- there *HAS* to be a better way to get git log to return stashes const stash = getSettledValue(stashResult); if (stash != null) { @@ -1629,259 +1663,206 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } - let getLogForRefFn; - if (options?.ref != null) { - const head = getSettledValue(headResult); - - async function getLogForRef(this: LocalGitProvider): Promise { - let log; - - const parser = GitLogParser.create<{ sha: string; date: string }>({ sha: '%H', date: '%ct' }); - const data = await this.git.log(repoPath, options?.ref, { argsOrFormat: parser.arguments, limit: 1 }); + async function getCommitsForGraphCore( + this: LocalGitProvider, + limit: number, + shaOrCursor?: string | { sha: string; timestamp: string }, + ): Promise { + let cursor: { sha: string; timestamp: string } | undefined; + let sha: string | undefined; + if (shaOrCursor != null) { + if (typeof shaOrCursor === 'string') { + sha = shaOrCursor; + } else { + cursor = shaOrCursor; + } + } - let commit = first(parser.parse(data)); - if (commit != null) { - const defaultItemLimit = configuration.get('graph.defaultItemLimit'); + let log: string | undefined; + let nextPageLimit = limit; + let size; - let found = false; - // If we are looking for the HEAD assume that it might be in the first page (so we can avoid extra queries) - if (options!.ref === 'HEAD' || options!.ref === head) { - log = await this.getLog(repoPath, { - all: options!.mode !== 'single', - ordering: 'date', - limit: defaultItemLimit, - stdin: stdin, - }); - found = log?.commits.has(commit.sha) ?? false; - } + do { + const args = [...parser.arguments, '-m', `--${ordering}-order`, '--all']; - if (!found) { - // Get the log up to (and including) the specified commit - log = await this.getLog(repoPath, { - all: options!.mode !== 'single', - ordering: 'date', - limit: 0, - extraArgs: [`--since="${Number(commit.date)}"`, '--boundary'], - stdin: stdin, - }); + if (since) { + args.push(`--since=${since}`, '--boundary'); + // Only allow `since` once + since = undefined; + } else { + args.push(`-n${nextPageLimit + 1}`); + if (cursor) { + args.push(`--until=${cursor.timestamp}`, '--boundary'); } + } - found = log?.commits.has(commit.sha) ?? false; - if (!found) { - Logger.debug(scope, `Could not find commit ${options!.ref}`); - - debugger; + let data = await this.git.log2(repoPath, undefined, stdin, ...args); + if (cursor || sha) { + const cursorIndex = data.startsWith(`${cursor?.sha ?? sha}\0`) + ? 0 + : data.indexOf(`\0\0${cursor?.sha ?? sha}\0`); + if (cursorIndex === -1) { + // If we didn't find any new commits, we must have them all so return that we have everything + if (size === data.length) return { repoPath: repoPath, rows: [] }; + + size = data.length; + nextPageLimit = (nextPageLimit === 0 ? defaultPageLimit : nextPageLimit) * 2; + continue; } - if (log?.more != null && (!found || log.commits.size < defaultItemLimit / 2)) { - Logger.debug(scope, 'Loading next page...'); - - log = await log.more( - (found && log.commits.size < defaultItemLimit / 2 - ? defaultItemLimit - : configuration.get('graph.pageItemLimit')) ?? options?.limit, - ); - // We need to clear the "pagedCommits", since we want to return the entire set - if (log != null) { - (log as Mutable).pagedCommits = undefined; + if (cursorIndex > 0 && cursor != null) { + const duplicates = data.substring(0, cursorIndex); + if (data.length - duplicates.length < (size ?? data.length) / 4) { + size = data.length; + nextPageLimit = (nextPageLimit === 0 ? defaultPageLimit : nextPageLimit) * 2; + continue; } - found = log?.commits.has(commit.sha) ?? false; - if (!found) { - Logger.debug(scope, `Still could not find commit ${options!.ref}`); - commit = undefined; - - debugger; - } - } + // Substract out any duplicate commits (regex is faster than parsing and counting) + nextPageLimit -= (duplicates.match(/\0\0[0-9a-f]{40}\0/g)?.length ?? 0) + 1; - if (!found) { - commit = undefined; + data = data.substring(cursorIndex + 2); } - - options!.ref = commit?.sha; } - return ( - log ?? - this.getLog(repoPath, { - all: options?.mode !== 'single', - ordering: 'date', - limit: options?.limit, - stdin: stdin, - }) - ); - } - - getLogForRefFn = getLogForRef; - } - const [logResult, remotesResult] = await Promise.allSettled([ - getLogForRefFn?.call(this) ?? - this.getLog(repoPath, { - all: options?.mode !== 'single', - ordering: 'date', - limit: options?.limit, - stdin: stdin, - }), - this.getRemotes(repoPath), - ]); - - return this.getCommitsForGraphCore( - repoPath, - asWebviewUri, - getSettledValue(logResult), - stash, - getSettledValue(remotesResult), - options, - ); - } + if (!data) return { repoPath: repoPath, rows: [] }; - private getCommitsForGraphCore( - repoPath: string, - asWebviewUri: (uri: Uri) => Uri, - log: GitLog | undefined, - stash: GitStash | undefined, - remotes: GitRemote[] | undefined, - options?: { - branch?: string; - limit?: number; - mode?: 'single' | 'local' | 'all'; - ref?: string; - }, - ): 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; + log = data; + if (limit !== 0) { + limit = nextPageLimit; + } - const remoteMap = remotes != null ? new Map(remotes.map(r => [r.name, r])) : new Map(); + break; + } while (true); - const skipStashParents = new Set(); + const rows: GitGraphRow[] = []; + + let current = false; + let refHeads: GitGraphRowHead[]; + let refRemoteHeads: GitGraphRowRemoteHead[]; + let refTags: GitGraphRowTag[]; + let parents: string[]; + let remoteName: string; + let isStashCommit: boolean; + + let commitCount = 0; + const startingCursor = cursor?.sha; + + const commits = parser.parse(log); + for (const commit of commits) { + commitCount++; + // If we are paging, skip the first commit since its a duplicate of the last commit from the previous page + if (startingCursor === commit.sha || skipStashParents.has(commit.sha)) continue; + + refHeads = []; + refRemoteHeads = []; + refTags = []; + + if (commit.tips) { + for (let tip of commit.tips.split(', ')) { + if (tip === 'refs/stash' || tip === 'HEAD') continue; + + if (tip.startsWith('tag: ')) { + refTags.push({ + name: tip.substring(5), + // Not currently used, so don't bother looking it up + annotated: true, + }); - for (const commit of commits) { - if (skipStashParents.has(commit.sha)) continue; + continue; + } - refHeads = []; - refRemoteHeads = []; - refTags = []; + current = tip.startsWith('HEAD -> '); + if (current) { + tip = tip.substring(8); + } - if (commit.tips != null) { - for (let tip of commit.tips) { - if (tip === 'refs/stash' || tip === 'HEAD') continue; + 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; + } + } - if (tip.startsWith('tag: ')) { - refTags.push({ - name: tip.substring(5), - // Not currently used, so don't bother filling it out - annotated: false, + refHeads.push({ + name: tip, + isCurrentHead: current, }); - - 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); + isStashCommit = 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]; + parents = commit.parents ? commit.parents.split(' ') : []; + // 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) { + // 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); + } - // 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, + avatarUrl: !isStashCommit ? getAvatarUri(commit.authorEmail, undefined).toString(true) : undefined, + email: commit.authorEmail ?? '', + date: Number(ordering === 'author-date' ? commit.authorDate : commit.committerDate) * 1000, + message: emojify(commit.message), + // 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, + }); } - rows.push({ - sha: commit.sha, - parents: parents, - author: commit.author.name, - avatarUrl: !isStashCommit ? getAvatarUri(commit.author.email, undefined).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, - }); + const last = rows[rows.length - 1]; + cursor = + last != null + ? { + sha: last.sha, + timestamp: String(Math.floor(last.date / 1000)), + } + : undefined; + + return { + repoPath: repoPath, + paging: { + limit: limit, + endingCursor: cursor?.timestamp, + startingCursor: startingCursor, + more: commitCount > limit, + }, + rows: rows, + sha: sha ?? head, + + more: async (limit: number): Promise => + getCommitsForGraphCore.call(this, limit, cursor), + }; } - return { - repoPath: repoPath, - paging: { - limit: log.limit, - endingCursor: log.endingCursor, - startingCursor: log.startingCursor, - more: log.hasMore, - }, - rows: rows, - sha: options?.ref, - - more: async (limit: number | { until: string } | undefined): Promise => { - const moreLog = await log.more?.(limit); - return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, stash, remotes, options); - }, - }; + return getCommitsForGraphCore.call(this, limit, selectSha); } @log() @@ -1917,7 +1898,7 @@ export class LocalGitProvider implements GitProvider, Disposable { repoPath = normalizePath(repoPath); const currentUser = await this.getCurrentUser(repoPath); - const parser = GitLogParser.create<{ + const parser = createLogParser<{ sha: string; author: string; email: string; @@ -2367,6 +2348,7 @@ export class LocalGitProvider implements GitProvider, Disposable { merges?: boolean; ordering?: 'date' | 'author-date' | 'topo' | null; ref?: string; + status?: null | 'name-status' | 'numstat' | 'stat'; since?: number | string; until?: number | string; extraArgs?: string[]; @@ -2383,11 +2365,13 @@ export class LocalGitProvider implements GitProvider, Disposable { const args = [ `--format=${options?.all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`, - '--name-status', - '--full-history', `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '-m', ]; + + if (options?.status !== null) { + args.push(`--${options?.status ?? 'name-status'}`, '--full-history'); + } if (options?.all) { args.push('--all'); } @@ -2518,7 +2502,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const limit = options?.limit ?? configuration.get('advanced.maxListItems') ?? 0; try { - const parser = GitLogParser.createSingle('%H'); + const parser = createLogParserSingle('%H'); const data = await this.git.log(repoPath, options?.ref, { authors: options?.authors, @@ -3703,7 +3687,7 @@ export class LocalGitProvider implements GitProvider, Disposable { let stash = this.useCaching ? this._stashesCache.get(repoPath) : undefined; if (stash === undefined) { - const parser = GitLogParser.createWithFiles<{ + const parser = createLogParserWithFiles<{ sha: string; date: string; committedDate: string; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 1e1759b..10eb27e 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -78,38 +78,220 @@ type ParserWithFiles = { parse: (data: string) => Generator>; }; +type GraphParser = Parser<{ + sha: string; + author: string; + authorEmail: string; + authorDate: string; + committerDate: string; + parents: string; + tips: string; + message: string; +}>; + +let _graphParser: GraphParser | undefined; +export function getGraphParser(): GraphParser { + if (_graphParser == null) { + _graphParser = createLogParser({ + sha: '%H', + author: '%aN', + authorEmail: '%aE', + authorDate: '%at', + committerDate: '%ct', + parents: '%P', + tips: '%D', + message: '%B', + }); + } + return _graphParser; +} + +type GraphRefParser = Parser<{ + sha: string; + authorDate: string; + committerDate: string; +}>; + +let _graphRefParser: GraphRefParser | undefined; +export function getGraphRefParser(): GraphRefParser { + if (_graphRefParser == null) { + _graphRefParser = createLogParser({ + sha: '%H', + authorDate: '%at', + committerDate: '%ct', + }); + } + return _graphRefParser; +} + +export function createLogParser>( + fieldMapping: ExtractAll, + options?: { + additionalArgs?: string[]; + parseEntry?: (fields: IterableIterator, entry: T) => void; + prefix?: string; + fieldPrefix?: string; + fieldSuffix?: string; + separator?: string; + skip?: number; + }, +): Parser { + let format = options?.prefix ?? ''; + const keys: (keyof ExtractAll)[] = []; + for (const key in fieldMapping) { + keys.push(key); + format += `${options?.fieldPrefix ?? ''}${fieldMapping[key]}${ + options?.fieldSuffix ?? (options?.fieldPrefix == null ? '%x00' : '') + }`; + } + + const args = ['-z', `--format=${format}`]; + if (options?.additionalArgs != null && options.additionalArgs.length > 0) { + args.push(...options.additionalArgs); + } + + function* parse(data: string): Generator { + let entry: T = {} as any; + let fieldCount = 0; + let field; + + const fields = getLines(data, options?.separator ?? '\0'); + if (options?.skip) { + for (let i = 0; i < options.skip; i++) { + fields.next(); + } + } + + while (true) { + field = fields.next(); + if (field.done) break; + + entry[keys[fieldCount++]] = field.value as T[keyof T]; + + if (fieldCount === keys.length) { + fieldCount = 0; + field = fields.next(); + + options?.parseEntry?.(fields, entry); + yield entry; + + entry = {} as any; + } + } + } + + return { arguments: args, parse: parse }; +} + +export function createLogParserSingle(field: string): Parser { + const format = field; + const args = ['-z', `--format=${format}`]; + + function* parse(data: string): Generator { + let field; + + const fields = getLines(data, '\0'); + while (true) { + field = fields.next(); + if (field.done) break; + + yield field.value; + } + } + + return { arguments: args, parse: parse }; +} + +export function createLogParserWithFiles>( + fieldMapping: ExtractAll, +): ParserWithFiles { + let format = '%x00'; + const keys: (keyof ExtractAll)[] = []; + for (const key in fieldMapping) { + keys.push(key); + format += `%x00${fieldMapping[key]}`; + } + + const args = ['-z', `--format=${format}`, '--name-status']; + + function* parse(data: string): Generator> { + const records = getLines(data, '\0\0\0'); + + let entry: ParsedEntryWithFiles; + let files: ParsedEntryFile[]; + let fields: IterableIterator; + + for (const record of records) { + entry = {} as any; + files = []; + fields = getLines(record, '\0'); + + // Skip the 2 starting NULs + fields.next(); + fields.next(); + + let fieldCount = 0; + let field; + while (true) { + field = fields.next(); + if (field.done) break; + + if (fieldCount < keys.length) { + entry[keys[fieldCount++]] = field.value as ParsedEntryWithFiles[keyof T]; + } else { + const file: ParsedEntryFile = { status: field.value.trim(), path: undefined! }; + field = fields.next(); + file.path = field.value; + + if (file.status[0] === 'R' || file.status[0] === 'C') { + field = fields.next(); + file.originalPath = field.value; + } + + files.push(file); + } + } + + entry.files = files; + yield entry; + } + } + + return { arguments: args, parse: parse }; +} + // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class GitLogParser { static readonly shortstatRegex = /(?\d+) files? changed(?:, (?\d+) insertions?\(\+\))?(?:, (?\d+) deletions?\(-\))?/; - private static _defaultParser: ParserWithFiles<{ - sha: string; - author: string; - authorEmail: string; - authorDate: string; - committer: string; - committerEmail: string; - committerDate: string; - message: string; - parents: string[]; - }>; - static get defaultParser() { - if (this._defaultParser == null) { - this._defaultParser = GitLogParser.createWithFiles({ - sha: '%H', - author: '%aN', - authorEmail: '%aE', - authorDate: '%at', - committer: '%cN', - committerEmail: '%cE', - committerDate: '%ct', - message: '%B', - parents: '%P', - }); - } - return this._defaultParser; - } + // private static _defaultParser: ParserWithFiles<{ + // sha: string; + // author: string; + // authorEmail: string; + // authorDate: string; + // committer: string; + // committerEmail: string; + // committerDate: string; + // message: string; + // parents: string[]; + // }>; + // static get defaultParser() { + // if (this._defaultParser == null) { + // this._defaultParser = GitLogParser.createWithFiles({ + // sha: '%H', + // author: '%aN', + // authorEmail: '%aE', + // authorDate: '%at', + // committer: '%cN', + // committerEmail: '%cE', + // committerDate: '%ct', + // message: '%B', + // parents: '%P', + // }); + // } + // return this._defaultParser; + // } static allFormat = [ `${lb}${sl}f${rb}`, @@ -149,140 +331,6 @@ export class GitLogParser { static shortlog = '%H%x00%aN%x00%aE%x00%at'; - static create>( - fieldMapping: ExtractAll, - options?: { - additionalArgs?: string[]; - parseEntry?: (fields: IterableIterator, entry: T) => void; - prefix?: string; - fieldPrefix?: string; - fieldSuffix?: string; - separator?: string; - skip?: number; - }, - ): Parser { - let format = options?.prefix ?? ''; - const keys: (keyof ExtractAll)[] = []; - for (const key in fieldMapping) { - keys.push(key); - format += `${options?.fieldPrefix ?? ''}${fieldMapping[key]}${ - options?.fieldSuffix ?? (options?.fieldPrefix == null ? '%x00' : '') - }`; - } - - const args = ['-z', `--format=${format}`]; - if (options?.additionalArgs != null && options.additionalArgs.length > 0) { - args.push(...options.additionalArgs); - } - - function* parse(data: string): Generator { - let entry: T = {} as any; - let fieldCount = 0; - let field; - - const fields = getLines(data, options?.separator ?? '\0'); - if (options?.skip) { - for (let i = 0; i < options.skip; i++) { - fields.next(); - } - } - - while (true) { - field = fields.next(); - if (field.done) break; - - entry[keys[fieldCount++]] = field.value as T[keyof T]; - - if (fieldCount === keys.length) { - fieldCount = 0; - field = fields.next(); - - options?.parseEntry?.(fields, entry); - yield entry; - - entry = {} as any; - } - } - } - - return { arguments: args, parse: parse }; - } - - static createSingle(field: string): Parser { - const format = field; - const args = ['-z', `--format=${format}`]; - - function* parse(data: string): Generator { - let field; - - const fields = getLines(data, '\0'); - while (true) { - field = fields.next(); - if (field.done) break; - - yield field.value; - } - } - - return { arguments: args, parse: parse }; - } - - static createWithFiles>(fieldMapping: ExtractAll): ParserWithFiles { - let format = '%x00'; - const keys: (keyof ExtractAll)[] = []; - for (const key in fieldMapping) { - keys.push(key); - format += `%x00${fieldMapping[key]}`; - } - - const args = ['-z', `--format=${format}`, '--name-status']; - - function* parse(data: string): Generator> { - const records = getLines(data, '\0\0\0'); - - let entry: ParsedEntryWithFiles; - let files: ParsedEntryFile[]; - let fields: IterableIterator; - - for (const record of records) { - entry = {} as any; - files = []; - fields = getLines(record, '\0'); - - // Skip the 2 starting NULs - fields.next(); - fields.next(); - - let fieldCount = 0; - let field; - while (true) { - field = fields.next(); - if (field.done) break; - - if (fieldCount < keys.length) { - entry[keys[fieldCount++]] = field.value as ParsedEntryWithFiles[keyof T]; - } else { - const file: ParsedEntryFile = { status: field.value.trim(), path: undefined! }; - field = fields.next(); - file.path = field.value; - - if (file.status[0] === 'R' || file.status[0] === 'C') { - field = fields.next(); - file.originalPath = field.value; - } - - files.push(file); - } - } - - entry.files = files; - yield entry; - } - } - - return { arguments: args, parse: parse }; - } - @debug({ args: false }) static parse( container: Container, diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 9d04c68..3614e0e 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -318,7 +318,7 @@ export class GraphWebview extends WebviewBase { const { defaultItemLimit, pageItemLimit } = this.getConfig(); const newGraph = await this._graph.more(limit ?? pageItemLimit ?? defaultItemLimit); if (newGraph != null) { - this.setGraph(newGraph); + this.setGraph(newGraph, true); } else { debugger; } @@ -514,12 +514,13 @@ export class GraphWebview extends WebviewBase { this._selectedSha = undefined; } - private setGraph(graph: GitGraph | undefined) { + private setGraph(graph: GitGraph | undefined, incremental?: boolean) { this._graph = graph; - if (graph == null) { + if (graph == null || !incremental) { this._ids.clear(); - return; + + if (graph == null) return; } // TODO@eamodio see if we can ask the graph if it can select the sha, so we don't have to maintain a set of ids