Quellcode durchsuchen

Optimizes graph log parsing & paging

Adds graph.commitOrdering setting
main
Eric Amodio vor 2 Jahren
Ursprung
Commit
1263b333ca
5 geänderte Dateien mit 458 neuen und 407 gelöschten Zeilen
  1. +23
    -6
      package.json
  2. +1
    -0
      src/config.ts
  3. +220
    -236
      src/env/node/git/localGitProvider.ts
  4. +209
    -161
      src/git/parsers/logParser.ts
  5. +5
    -4
      src/plus/webviews/graph/graphWebview.ts

+ 23
- 6
package.json Datei anzeigen

@ -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
}
}
},

+ 1
- 0
src/config.ts Datei anzeigen

@ -378,6 +378,7 @@ export interface GraphColumnConfig {
}
export interface GraphConfig {
commitOrdering: 'date' | 'author-date' | 'topo';
defaultItemLimit: number;
pageItemLimit: number;
statusBar: {

+ 220
- 236
src/env/node/git/localGitProvider.ts Datei anzeigen

@ -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<GitGraph> {
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<GitLog | undefined> {
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<GitGraph> {
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<typeof log>).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<GitGraph | undefined> =>
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<GitGraph | undefined> => {
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;

+ 209
- 161
src/git/parsers/logParser.ts Datei anzeigen

@ -78,38 +78,220 @@ type ParserWithFiles = {
parse: (data: string) => Generator<ParsedEntryWithFiles<T>>;
};
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<T extends Record<string, unknown>>(
fieldMapping: ExtractAll<T, string>,
options?: {
additionalArgs?: string[];
parseEntry?: (fields: IterableIterator<string>, entry: T) => void;
prefix?: string;
fieldPrefix?: string;
fieldSuffix?: string;
separator?: string;
skip?: number;
},
): Parser<T> {
let format = options?.prefix ?? '';
const keys: (keyof ExtractAll<T, string>)[] = [];
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<T> {
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<string> {
const format = field;
const args = ['-z', `--format=${format}`];
function* parse(data: string): Generator<string> {
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<T extends Record<string, unknown>>(
fieldMapping: ExtractAll<T, string>,
): ParserWithFiles<T> {
let format = '%x00';
const keys: (keyof ExtractAll<T, string>)[] = [];
for (const key in fieldMapping) {
keys.push(key);
format += `%x00${fieldMapping[key]}`;
}
const args = ['-z', `--format=${format}`, '--name-status'];
function* parse(data: string): Generator<ParsedEntryWithFiles<T>> {
const records = getLines(data, '\0\0\0');
let entry: ParsedEntryWithFiles<T>;
let files: ParsedEntryFile[];
let fields: IterableIterator<string>;
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<T>[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 =
/(?<files>\d+) files? changed(?:, (?<additions>\d+) insertions?\(\+\))?(?:, (?<deletions>\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<T extends Record<string, unknown>>(
fieldMapping: ExtractAll<T, string>,
options?: {
additionalArgs?: string[];
parseEntry?: (fields: IterableIterator<string>, entry: T) => void;
prefix?: string;
fieldPrefix?: string;
fieldSuffix?: string;
separator?: string;
skip?: number;
},
): Parser<T> {
let format = options?.prefix ?? '';
const keys: (keyof ExtractAll<T, string>)[] = [];
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<T> {
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<string> {
const format = field;
const args = ['-z', `--format=${format}`];
function* parse(data: string): Generator<string> {
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<T extends Record<string, unknown>>(fieldMapping: ExtractAll<T, string>): ParserWithFiles<T> {
let format = '%x00';
const keys: (keyof ExtractAll<T, string>)[] = [];
for (const key in fieldMapping) {
keys.push(key);
format += `%x00${fieldMapping[key]}`;
}
const args = ['-z', `--format=${format}`, '--name-status'];
function* parse(data: string): Generator<ParsedEntryWithFiles<T>> {
const records = getLines(data, '\0\0\0');
let entry: ParsedEntryWithFiles<T>;
let files: ParsedEntryFile[];
let fields: IterableIterator<string>;
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<T>[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,

+ 5
- 4
src/plus/webviews/graph/graphWebview.ts Datei anzeigen

@ -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

Laden…
Abbrechen
Speichern