From d2c0d43daa7b8566ad5dc48d51463fda4412e044 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 31 Dec 2022 19:35:48 -0500 Subject: [PATCH] Adds ability to include stats on graph git call --- src/env/node/git/git.ts | 8 ++- src/env/node/git/localGitProvider.ts | 78 +++++----------------- src/git/gitProvider.ts | 2 +- src/git/gitProviderService.ts | 2 +- src/git/models/graph.ts | 7 ++ src/git/parsers/logParser.ts | 122 +++++++++++++++++++++++++++++------ src/plus/github/githubGitProvider.ts | 2 +- 7 files changed, 135 insertions(+), 86 deletions(-) diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 6a273fe..4478f39 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -907,7 +907,7 @@ export class Git { '--', ); - const shaRegex = new RegExp(`(?:^|\x00\x00)${sha}\x00`); + const shaRegex = getShaInLogRegex(sha); let found = false; let count = 0; @@ -935,7 +935,7 @@ export class Git { function onData(s: string) { data.push(s); // eslint-disable-next-line no-control-regex - count += s.match(/(?:^|\x00\x00)[0-9a-f]{40}\x00/g)?.length ?? 0; + count += s.match(/(?:^\x00*|\x00\x00)[0-9a-f]{40}\x00/g)?.length ?? 0; if (!found && shaRegex.test(s)) { found = true; @@ -1829,3 +1829,7 @@ export class Git { } } } + +export function getShaInLogRegex(sha: string) { + return new RegExp(`(?:^\x00*|\x00\x00)${sha}\x00`); +} diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index d6cda36..5ffebbf 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -92,9 +92,9 @@ import { GitBlameParser } from '../../../git/parsers/blameParser'; import { GitBranchParser } from '../../../git/parsers/branchParser'; import { GitDiffParser } from '../../../git/parsers/diffParser'; import { - createLogParser, createLogParserSingle, createLogParserWithFiles, + getContributorsParser, getGraphParser, getRefAndDateParser, getRefParser, @@ -155,7 +155,13 @@ import { serializeWebviewItemContext } from '../../../system/webview'; import type { CachedBlame, CachedDiff, CachedLog, TrackedDocument } from '../../../trackers/gitDocumentTracker'; import { GitDocumentState } from '../../../trackers/gitDocumentTracker'; import type { Git } from './git'; -import { GitErrors, gitLogDefaultConfigs, gitLogDefaultConfigsWithFiles, maxGitCliLength } from './git'; +import { + getShaInLogRegex, + GitErrors, + gitLogDefaultConfigs, + gitLogDefaultConfigsWithFiles, + maxGitCliLength, +} from './git'; import type { GitLocation } from './locator'; import { findGitPath, InvalidGitConfigError, UnableToFindGitError } from './locator'; import { CancelledRunError, fsExists, RunError } from './shell'; @@ -1629,12 +1635,12 @@ export class LocalGitProvider implements GitProvider, Disposable { asWebviewUri: (uri: Uri) => Uri, options?: { branch?: string; + include?: { stats?: boolean }; limit?: number; - mode?: 'single' | 'local' | 'all'; ref?: string; }, ): Promise { - const parser = getGraphParser(); + const parser = getGraphParser(options?.include?.stats); const refParser = getRefParser(); const defaultLimit = options?.limit ?? configuration.get('graph.defaultItemLimit') ?? 5000; @@ -1710,10 +1716,7 @@ export class LocalGitProvider implements GitProvider, Disposable { data = await this.git.log2(repoPath, stdin ? { stdin: stdin } : undefined, ...args); if (cursor) { - const cursorIndex = data.startsWith(`${cursor.sha}\x00`) - ? 0 - : data.indexOf(`\x00\x00${cursor.sha}\x00`); - if (cursorIndex === -1) { + if (!getShaInLogRegex(cursor.sha).test(data)) { // If we didn't find any new commits, we must have them all so return that we have everything if (size === data.length) { return { @@ -1732,32 +1735,19 @@ export class LocalGitProvider implements GitProvider, Disposable { continue; } - - // 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; - // } - - // // 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; - - // data = data.substring(cursorIndex + 2); - // } } } - if (!data) - {return { + if (!data) { + return { repoPath: repoPath, avatars: avatars, ids: ids, branches: branchMap, remotes: remoteMap, rows: [], - };} + }; + } log = data; if (limit !== 0) { @@ -2053,6 +2043,7 @@ export class LocalGitProvider implements GitProvider, Disposable { remotes: refRemoteHeads, tags: refTags, contexts: contexts, + stats: commit.stats, }); } @@ -2121,42 +2112,7 @@ export class LocalGitProvider implements GitProvider, Disposable { try { repoPath = normalizePath(repoPath); const currentUser = await this.getCurrentUser(repoPath); - - const parser = createLogParser<{ - sha: string; - author: string; - email: string; - date: string; - stats?: { files: number; additions: number; deletions: number }; - }>( - { - sha: '%H', - author: '%aN', - email: '%aE', - date: '%at', - }, - options?.stats - ? { - additionalArgs: ['--shortstat', '--use-mailmap'], - parseEntry: (fields, entry) => { - const line = fields.next().value; - const match = GitLogParser.shortstatRegex.exec(line); - if (match?.groups != null) { - const { files, additions, deletions } = match.groups; - entry.stats = { - files: Number(files || 0), - additions: Number(additions || 0), - deletions: Number(deletions || 0), - }; - } - return entry; - }, - prefix: '%x00', - fieldSuffix: '%x00', - skip: 1, - } - : undefined, - ); + const parser = getContributorsParser(options?.stats); const data = await this.git.log(repoPath, options?.ref, { all: options?.all, diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 5b7fbb1..6d97d8d 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -224,8 +224,8 @@ export interface GitProvider extends Disposable { asWebviewUri: (uri: Uri) => Uri, options?: { branch?: string; + include?: { stats?: boolean }; limit?: number; - mode?: 'single' | 'local' | 'all'; ref?: string; }, ): Promise; diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 02d3d9b..42b2527 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -1410,8 +1410,8 @@ export class GitProviderService implements Disposable { asWebviewUri: (uri: Uri) => Uri, options?: { branch?: string; + include?: { stats?: boolean }; limit?: number; - mode?: 'single' | 'local' | 'all'; ref?: string; }, ): Promise { diff --git a/src/git/models/graph.ts b/src/git/models/graph.ts index 1be4e76..c25b706 100644 --- a/src/git/models/graph.ts +++ b/src/git/models/graph.ts @@ -15,12 +15,19 @@ export const enum GitGraphRowType { Rebase = 'unsupported-rebase-warning-node', } +export interface GitGraphRowStats { + files: number; + additions: number; + deletions: number; +} + export interface GitGraphRow extends GraphRow { type: GitGraphRowType; heads?: GitGraphRowHead[]; remotes?: GitGraphRowRemoteHead[]; tags?: GitGraphRowTag[]; contexts?: GitGraphRowContexts; + stats?: GitGraphRowStats; } export interface GitGraph { diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 581c5fa..4d2a2a4 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -25,6 +25,9 @@ const logFileSimpleRegex = /^ (.*)\s*(?:(?:diff --git a\/(.*) b\/(.*))|(?:(\S const logFileSimpleRenamedRegex = /^ (\S+)\s*(.*)$/s; const logFileSimpleRenamedFilesRegex = /^(\S)\S*\t([^\t\n]+)(?:\t(.+)?)?$/gm; +const shortstatRegex = + /(?\d+) files? changed(?:, (?\d+) insertions?\(\+\))?(?:, (?\d+) deletions?\(-\))?/; + // Using %x00 codes because some shells seem to try to expand things if not const lb = '%x3c'; // `%x${'<'.charCodeAt(0).toString(16)}`; const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`; @@ -71,14 +74,48 @@ export type Parser = { parse: (data: string | string[]) => Generator; }; -type ParsedEntryFile = { status: string; path: string; originalPath?: string }; -type ParsedEntryWithFiles = { [K in keyof T]: string } & { files: ParsedEntryFile[] }; -type ParserWithFiles = { - arguments: string[]; - parse: (data: string) => Generator>; -}; +export type ParsedEntryFile = { status: string; path: string; originalPath?: string }; +export type ParsedEntryWithFiles = { [K in keyof T]: string } & { files: ParsedEntryFile[] }; +export type ParserWithFiles = Parser>; + +export type ParsedStats = { files: number; additions: number; deletions: number }; +export type ParsedEntryWithStats = T & { stats?: ParsedStats }; +export type ParserWithStats = Parser>; -type GraphParser = Parser<{ +type ContributorsParserMaybeWithStats = ParserWithStats<{ + sha: string; + author: string; + email: string; + date: string; +}>; + +let _contributorsParser: ContributorsParserMaybeWithStats | undefined; +let _contributorsParserWithStats: ContributorsParserMaybeWithStats | undefined; +export function getContributorsParser(stats?: boolean): ContributorsParserMaybeWithStats { + if (stats) { + if (_contributorsParserWithStats == null) { + _contributorsParserWithStats = createLogParserWithStats({ + sha: '%H', + author: '%aN', + email: '%aE', + date: '%at', + }); + } + return _contributorsParserWithStats; + } + + if (_contributorsParser == null) { + _contributorsParser = createLogParser({ + sha: '%H', + author: '%aN', + email: '%aE', + date: '%at', + }); + } + return _contributorsParser; +} + +type GraphParserMaybeWithStats = ParserWithStats<{ sha: string; author: string; authorEmail: string; @@ -89,8 +126,26 @@ type GraphParser = Parser<{ message: string; }>; -let _graphParser: GraphParser | undefined; -export function getGraphParser(): GraphParser { +let _graphParser: GraphParserMaybeWithStats | undefined; +let _graphParserWithStats: GraphParserMaybeWithStats | undefined; + +export function getGraphParser(stats?: boolean): GraphParserMaybeWithStats { + if (stats) { + if (_graphParserWithStats == null) { + _graphParserWithStats = createLogParserWithStats({ + sha: '%H', + author: '%aN', + authorEmail: '%aE', + authorDate: '%at', + committerDate: '%ct', + parents: '%P', + tips: '%D', + message: '%B', + }); + } + return _graphParserWithStats; + } + if (_graphParser == null) { _graphParser = createLogParser({ sha: '%H', @@ -130,18 +185,21 @@ export function getRefAndDateParser(): RefAndDateParser { return _refAndDateParser; } -export function createLogParser>( +export function createLogParser< + T extends Record, + TAdditional extends Record = Record, +>( fieldMapping: ExtractAll, options?: { additionalArgs?: string[]; - parseEntry?: (fields: IterableIterator, entry: T) => void; + parseEntry?: (fields: IterableIterator, entry: T & TAdditional) => void; prefix?: string; fieldPrefix?: string; fieldSuffix?: string; separator?: string; skip?: number; }, -): Parser { +): Parser { let format = options?.prefix ?? ''; const keys: (keyof ExtractAll)[] = []; for (const key in fieldMapping) { @@ -156,15 +214,15 @@ export function createLogParser>( args.push(...options.additionalArgs); } - function* parse(data: string | string[]): Generator { - let entry: T = {} as any; + function* parse(data: string | string[]): Generator { + let entry: T & TAdditional = {} 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(); + field = fields.next(); } } @@ -172,7 +230,7 @@ export function createLogParser>( field = fields.next(); if (field.done) break; - entry[keys[fieldCount++]] = field.value as T[keyof T]; + entry[keys[fieldCount++]] = field.value as (T & TAdditional)[keyof T]; if (fieldCount === keys.length) { fieldCount = 0; @@ -220,7 +278,7 @@ export function createLogParserWithFiles>( const args = ['-z', `--format=${format}`, '--name-status']; - function* parse(data: string): Generator> { + function* parse(data: string | string[]): Generator> { const records = getLines(data, '\0\0\0'); let entry: ParsedEntryWithFiles; @@ -266,11 +324,35 @@ export function createLogParserWithFiles>( return { arguments: args, parse: parse }; } +export function createLogParserWithStats>( + fieldMapping: ExtractAll, +): ParserWithStats { + function parseStats(fields: IterableIterator, entry: ParsedEntryWithStats) { + const stats = fields.next().value; + const match = shortstatRegex.exec(stats); + if (match?.groups != null) { + entry.stats = { + files: Number(match.groups.files || 0), + additions: Number(match.groups.additions || 0), + deletions: Number(match.groups.deletions || 0), + }; + } + fields.next(); + return entry; + } + + return createLogParser>(fieldMapping, { + additionalArgs: ['--shortstat'], + parseEntry: parseStats, + prefix: '%x00%x00', + separator: '\0', + fieldSuffix: '%x00', + skip: 2, + }); +} + // 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; diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index b55fbac..5d34fe2 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -1072,8 +1072,8 @@ export class GitHubGitProvider implements GitProvider, Disposable { asWebviewUri: (uri: Uri) => Uri, options?: { branch?: string; + include?: { stats?: boolean }; limit?: number; - mode?: 'single' | 'local' | 'all'; ref?: string; }, ): Promise {