diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index f1e4c06..b5496f1 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -48,7 +48,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { const blame = await this.getBlame(); if (blame == null) return false; - const sw = maybeStopWatch(scope); + using sw = maybeStopWatch(scope); const cfg = configuration.get('blame'); diff --git a/src/annotations/gutterChangesAnnotationProvider.ts b/src/annotations/gutterChangesAnnotationProvider.ts index 6539b0b..e92ed53 100644 --- a/src/annotations/gutterChangesAnnotationProvider.ts +++ b/src/annotations/gutterChangesAnnotationProvider.ts @@ -156,7 +156,7 @@ export class GutterChangesAnnotationProvider extends AnnotationProviderBase(d?: T): d is T => Boolean(d)); if (!diffs?.length) return false; - const sw = maybeStopWatch(scope); + using sw = maybeStopWatch(scope); const decorationsMap = new Map< string, diff --git a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts index 09e6881..d6d9a21 100644 --- a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts +++ b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts @@ -26,7 +26,7 @@ export class GutterHeatmapBlameAnnotationProvider extends BlameAnnotationProvide const blame = await this.getBlame(); if (blame == null) return false; - const sw = maybeStopWatch(scope); + using sw = maybeStopWatch(scope); const decorationsMap = new Map< string, diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index e6caf42..8d93a4e 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -24,11 +24,11 @@ import type { GitDir } from '../../../git/gitProvider'; import type { GitDiffFilter } from '../../../git/models/diff'; import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference'; import type { GitUser } from '../../../git/models/user'; -import { GitBranchParser } from '../../../git/parsers/branchParser'; -import { GitLogParser } from '../../../git/parsers/logParser'; -import { GitReflogParser } from '../../../git/parsers/reflogParser'; +import { parseGitBranchesDefaultFormat } from '../../../git/parsers/branchParser'; +import { parseGitLogAllFormat, parseGitLogDefaultFormat } from '../../../git/parsers/logParser'; +import { parseGitRefLogDefaultFormat } from '../../../git/parsers/reflogParser'; import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser'; -import { GitTagParser } from '../../../git/parsers/tagParser'; +import { parseGitTagsDefaultFormat } from '../../../git/parsers/tagParser'; import { splitAt } from '../../../system/array'; import { configuration } from '../../../system/configuration'; import { log } from '../../../system/decorators/log'; @@ -1011,7 +1011,7 @@ export class Git { } for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { - const params = ['for-each-ref', `--format=${GitBranchParser.defaultFormat}`, 'refs/heads']; + const params = ['for-each-ref', `--format=${parseGitBranchesDefaultFormat}`, 'refs/heads']; if (options.all) { params.push('refs/remotes'); } @@ -1045,7 +1045,7 @@ export class Git { }, ) { if (argsOrFormat == null) { - argsOrFormat = ['--name-status', `--format=${all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`]; + argsOrFormat = ['--name-status', `--format=${all ? parseGitLogAllFormat : parseGitLogDefaultFormat}`]; } if (typeof argsOrFormat === 'string') { @@ -1242,7 +1242,7 @@ export class Git { const [file, root] = splitPath(fileName, repoPath, true); if (argsOrFormat == null) { - argsOrFormat = [`--format=${all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`]; + argsOrFormat = [`--format=${all ? parseGitLogAllFormat : parseGitLogDefaultFormat}`]; } if (typeof argsOrFormat === 'string') { @@ -1438,7 +1438,7 @@ export class Git { 'show', '--stdin', '--name-status', - `--format=${GitLogParser.defaultFormat}`, + `--format=${parseGitLogDefaultFormat}`, '--use-mailmap', ); } @@ -1451,7 +1451,7 @@ export class Git { 'log', ...(options?.stdin ? ['--stdin'] : emptyArray), '--name-status', - `--format=${GitLogParser.defaultFormat}`, + `--format=${parseGitLogDefaultFormat}`, '--use-mailmap', ...search, ...(options?.ordering ? [`--${options.ordering}-order`] : emptyArray), @@ -1542,7 +1542,7 @@ export class Git { skip?: number; } = {}, ): Promise { - const params = ['log', '--walk-reflogs', `--format=${GitReflogParser.defaultFormat}`, '--date=iso8601']; + const params = ['log', '--walk-reflogs', `--format=${parseGitRefLogDefaultFormat}`, '--date=iso8601']; if (ordering) { params.push(`--${ordering}-order`); @@ -2139,7 +2139,7 @@ export class Git { } tag(repoPath: string) { - return this.git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); + return this.git({ cwd: repoPath }, 'tag', '-l', `--format=${parseGitTagsDefaultFormat}`); } worktree__add( diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index c3509b9..af4da17 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -108,9 +108,9 @@ import type { GitTreeEntry } from '../../../git/models/tree'; import type { GitUser } from '../../../git/models/user'; import { isUserMatch } from '../../../git/models/user'; import type { GitWorktree } from '../../../git/models/worktree'; -import { GitBlameParser } from '../../../git/parsers/blameParser'; -import { GitBranchParser } from '../../../git/parsers/branchParser'; -import { parseDiffNameStatusFiles, parseDiffShortStat, parseFileDiff } from '../../../git/parsers/diffParser'; +import { parseGitBlame } from '../../../git/parsers/blameParser'; +import { parseGitBranches } from '../../../git/parsers/branchParser'; +import { parseGitDiffNameStatusFiles, parseGitDiffShortStat, parseGitFileDiff } from '../../../git/parsers/diffParser'; import { createLogParserSingle, createLogParserWithFiles, @@ -119,15 +119,20 @@ import { getGraphStatsParser, getRefAndDateParser, getRefParser, - GitLogParser, LogType, + parseGitLog, + parseGitLogAllFormat, + parseGitLogDefaultFormat, + parseGitLogSimple, + parseGitLogSimpleFormat, + parseGitLogSimpleRenamed, } from '../../../git/parsers/logParser'; -import { GitReflogParser } from '../../../git/parsers/reflogParser'; -import { GitRemoteParser } from '../../../git/parsers/remoteParser'; -import { GitStatusParser } from '../../../git/parsers/statusParser'; -import { GitTagParser } from '../../../git/parsers/tagParser'; -import { GitTreeParser } from '../../../git/parsers/treeParser'; -import { GitWorktreeParser } from '../../../git/parsers/worktreeParser'; +import { parseGitRefLog } from '../../../git/parsers/reflogParser'; +import { parseGitRemotes } from '../../../git/parsers/remoteParser'; +import { parseGitStatus } from '../../../git/parsers/statusParser'; +import { parseGitTags } from '../../../git/parsers/tagParser'; +import { parseGitTree } from '../../../git/parsers/treeParser'; +import { parseGitWorktrees } from '../../../git/parsers/worktreeParser'; import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/remoteProviders'; import type { GitSearch, GitSearchResultData, GitSearchResults, SearchQuery } from '../../../git/search'; import { getGitArgsFromSearchQuery, getSearchQueryComparisonKey } from '../../../git/search'; @@ -954,7 +959,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // Now check if that commit had any renames data = await this.git.log__file(repoPath, '.', ref, { - argsOrFormat: GitLogParser.simpleFormat, + argsOrFormat: parseGitLogSimpleFormat, fileMode: 'simple', filters: ['R', 'C', 'D'], limit: 1, @@ -962,7 +967,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }); if (data == null || data.length === 0) break; - const [foundRef, foundFile, foundStatus] = GitLogParser.parseSimpleRenamed(data, relativePath); + const [foundRef, foundFile, foundStatus] = parseGitLogSimpleRenamed(data, relativePath); if (foundStatus === 'D' && foundFile != null) return undefined; if (foundRef == null || foundFile == null) break; @@ -1455,7 +1460,7 @@ export class LocalGitProvider implements GitProvider, Disposable { args: configuration.get('advanced.blame.customArguments'), ignoreWhitespace: configuration.get('blame.ignoreWhitespace'), }); - const blame = GitBlameParser.parse(this.container, data, root, await this.getCurrentUser(root)); + const blame = parseGitBlame(this.container, data, root, await this.getCurrentUser(root)); return blame; } catch (ex) { // Trap and cache expected blame errors @@ -1536,7 +1541,7 @@ export class LocalGitProvider implements GitProvider, Disposable { correlationKey: `:${key}`, ignoreWhitespace: configuration.get('blame.ignoreWhitespace'), }); - const blame = GitBlameParser.parse(this.container, data, root, await this.getCurrentUser(root)); + const blame = parseGitBlame(this.container, data, root, await this.getCurrentUser(root)); return blame; } catch (ex) { // Trap and cache expected blame errors @@ -1601,7 +1606,7 @@ export class LocalGitProvider implements GitProvider, Disposable { startLine: lineToBlame, endLine: lineToBlame, }); - const blame = GitBlameParser.parse(this.container, data, root, await this.getCurrentUser(root)); + const blame = parseGitBlame(this.container, data, root, await this.getCurrentUser(root)); if (blame == null) return undefined; return { @@ -1652,7 +1657,7 @@ export class LocalGitProvider implements GitProvider, Disposable { startLine: lineToBlame, endLine: lineToBlame, }); - const blame = GitBlameParser.parse(this.container, data, root, await this.getCurrentUser(root)); + const blame = parseGitBlame(this.container, data, root, await this.getCurrentUser(root)); if (blame == null) return undefined; return { @@ -1819,7 +1824,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return current != null ? { values: [current] } : emptyPagedResult; } - return { values: GitBranchParser.parse(this.container, data, repoPath!) }; + return { values: parseGitBranches(this.container, data, repoPath!) }; } catch (ex) { this._branchesCache.delete(repoPath!); @@ -1856,7 +1861,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const data = await this.git.diff__shortstat(repoPath, ref); if (!data) return undefined; - return parseDiffShortStat(data); + return parseGitDiffShortStat(data); } @log() @@ -2752,7 +2757,7 @@ export class LocalGitProvider implements GitProvider, Disposable { similarityThreshold: configuration.get('advanced.similarityThreshold'), }); - const diff = parseFileDiff(data); + const diff = parseGitFileDiff(data); return diff; } catch (ex) { // Trap and cache expected diff errors @@ -2839,7 +2844,7 @@ export class LocalGitProvider implements GitProvider, Disposable { similarityThreshold: configuration.get('advanced.similarityThreshold'), }); - const diff = parseFileDiff(data); + const diff = parseGitFileDiff(data); return diff; } catch (ex) { // Trap and cache expected diff errors @@ -2895,7 +2900,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }); if (!data) return undefined; - const files = parseDiffNameStatusFiles(data, repoPath); + const files = parseGitDiffNameStatusFiles(data, repoPath); return files == null || files.length === 0 ? undefined : files; } catch (ex) { return undefined; @@ -2911,7 +2916,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const data = await this.git.show__name_status(root, relativePath, ref); if (!data) return undefined; - const files = parseDiffNameStatusFiles(data, repoPath); + const files = parseGitDiffNameStatusFiles(data, repoPath); if (files == null || files.length === 0) return undefined; return files[0]; @@ -2986,7 +2991,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const similarityThreshold = configuration.get('advanced.similarityThreshold'); const args = [ - `--format=${options?.all ? GitLogParser.allFormat : GitLogParser.defaultFormat}`, + `--format=${options?.all ? parseGitLogAllFormat : parseGitLogDefaultFormat}`, `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '-m', ]; @@ -3076,7 +3081,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // ); // } - const log = GitLogParser.parse( + const log = parseGitLog( this.container, data, LogType.Log, @@ -3461,7 +3466,7 @@ export class LocalGitProvider implements GitProvider, Disposable { startLine: range == null ? undefined : range.start.line + 1, endLine: range == null ? undefined : range.end.line + 1, }); - const log = GitLogParser.parse( + const log = parseGitLog( this.container, data, // If this is the log of a folder, parse it as a normal log rather than a file log @@ -3815,7 +3820,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const relativePath = this.getRelativePath(uri, repoPath); let data = await this.git.log__file(repoPath, relativePath, ref, { - argsOrFormat: GitLogParser.simpleFormat, + argsOrFormat: parseGitLogSimpleFormat, fileMode: 'simple', filters: filters, limit: skip + 1, @@ -3825,11 +3830,11 @@ export class LocalGitProvider implements GitProvider, Disposable { }); if (data == null || data.length === 0) return undefined; - const [nextRef, file, status] = GitLogParser.parseSimple(data, skip); + const [nextRef, file, status] = parseGitLogSimple(data, skip); // If the file was deleted, check for a possible rename if (status === 'D') { data = await this.git.log__file(repoPath, '.', nextRef, { - argsOrFormat: GitLogParser.simpleFormat, + argsOrFormat: parseGitLogSimpleFormat, fileMode: 'simple', filters: ['R', 'C'], limit: 1, @@ -3840,7 +3845,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return GitUri.fromFile(file ?? relativePath, repoPath, nextRef); } - const [nextRenamedRef, renamedFile] = GitLogParser.parseSimpleRenamed(data, file ?? relativePath); + const [nextRenamedRef, renamedFile] = parseGitLogSimpleRenamed(data, file ?? relativePath); return GitUri.fromFile( renamedFile ?? file ?? relativePath, repoPath, @@ -4085,7 +4090,7 @@ export class LocalGitProvider implements GitProvider, Disposable { let data; try { data = await this.git.log__file(repoPath, relativePath, ref, { - argsOrFormat: GitLogParser.simpleFormat, + argsOrFormat: parseGitLogSimpleFormat, fileMode: 'simple', limit: skip + 2, ordering: configuration.get('advanced.commitOrdering'), @@ -4113,7 +4118,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } if (data == null || data.length === 0) return undefined; - const [previousRef, file] = GitLogParser.parseSimple(data, skip, ref); + const [previousRef, file] = parseGitLogSimple(data, skip, ref); // If the previous ref matches the ref we asked for assume we are at the end of the history if (ref != null && ref === previousRef) return undefined; @@ -4143,7 +4148,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }); if (data == null) return undefined; - const reflog = GitReflogParser.parse(data, repoPath, reflogCommands, limit, limit * 100); + const reflog = parseGitRefLog(data, repoPath, reflogCommands, limit, limit * 100); if (reflog?.hasMore) { reflog.more = this.getReflogMoreFn(reflog, options); } @@ -4209,13 +4214,11 @@ export class LocalGitProvider implements GitProvider, Disposable { try { const data = await this.git.remote(repoPath!); - const remotes = GitRemoteParser.parse( + const remotes = parseGitRemotes( data, repoPath!, getRemoteProviderMatcher(this.container, providers), ); - if (remotes == null) return []; - return remotes; } catch (ex) { this._remotesCache.delete(repoPath!); @@ -4341,7 +4344,7 @@ export class LocalGitProvider implements GitProvider, Disposable { similarityThreshold: configuration.get('advanced.similarityThreshold'), }); - const status = GitStatusParser.parse(data, root, porcelainVersion); + const status = parseGitStatus(data, root, porcelainVersion); return status?.files?.[0]; } @@ -4355,7 +4358,7 @@ export class LocalGitProvider implements GitProvider, Disposable { similarityThreshold: configuration.get('advanced.similarityThreshold'), }); - const status = GitStatusParser.parse(data, root, porcelainVersion); + const status = parseGitStatus(data, root, porcelainVersion); return status?.files ?? []; } @@ -4368,7 +4371,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const data = await this.git.status(repoPath, porcelainVersion, { similarityThreshold: configuration.get('advanced.similarityThreshold'), }); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + const status = parseGitStatus(data, repoPath, porcelainVersion); if (status?.detached) { const rebaseStatus = await this.getRebaseStatus(repoPath); @@ -4399,7 +4402,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async function load(this: LocalGitProvider): Promise> { try { const data = await this.git.tag(repoPath!); - return { values: GitTagParser.parse(data, repoPath!) ?? [] }; + return { values: parseGitTags(data, repoPath!) }; } catch (ex) { this._tagsCache.delete(repoPath!); @@ -4436,8 +4439,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const [relativePath, root] = splitPath(path, repoPath); const data = await this.git.ls_tree(root, ref, relativePath); - const trees = GitTreeParser.parse(data); - return trees?.length ? trees[0] : undefined; + return parseGitTree(data)[0]; } @log() @@ -4445,7 +4447,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (repoPath == null) return []; const data = await this.git.ls_tree(repoPath, ref); - return GitTreeParser.parse(data) ?? []; + return parseGitTree(data); } @log({ args: { 1: false } }) @@ -4811,7 +4813,7 @@ export class LocalGitProvider implements GitProvider, Disposable { shas: shas, stdin: stdin, }); - const log = GitLogParser.parse( + const log = parseGitLog( this.container, data, LogType.Log, @@ -5212,7 +5214,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ); const data = await this.git.worktree__list(repoPath); - return GitWorktreeParser.parse(data, repoPath); + return parseGitWorktrees(data, repoPath); } // eslint-disable-next-line @typescript-eslint/require-await diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts index 7b202d8..fa770c8 100644 --- a/src/git/models/diff.ts +++ b/src/git/models/diff.ts @@ -1,4 +1,4 @@ -import { parseDiffHunk } from '../parsers/diffParser'; +import { parseGitDiffHunk } from '../parsers/diffParser'; export interface GitDiffLine { line: string; @@ -35,7 +35,7 @@ export class GitDiffHunk { private parsedHunk: { lines: GitDiffHunkLine[]; state: 'added' | 'changed' | 'removed' } | undefined; private parseHunk() { if (this.parsedHunk == null) { - this.parsedHunk = parseDiffHunk(this); + this.parsedHunk = parseGitDiffHunk(this); } return this.parsedHunk; } diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index b673a07..60d4d69 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -1,5 +1,5 @@ import type { Container } from '../../container'; -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import { getLines } from '../../system/string'; import type { GitBlame, GitBlameAuthor } from '../models/blame'; import type { GitCommitLine } from '../models/commit'; @@ -34,228 +34,223 @@ interface BlameEntry { summary?: string; } -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitBlameParser { - @debug({ args: false, singleLine: true }) - static parse( - container: Container, - data: string, - repoPath: string, - currentUser: GitUser | undefined, - ): GitBlame | undefined { - if (!data) return undefined; - - const authors = new Map(); - const commits = new Map(); - const lines: GitCommitLine[] = []; - - let entry: BlameEntry | undefined = undefined; - let key: string; - let line: string; - let lineParts: string[]; - - for (line of getLines(data)) { - lineParts = line.split(' '); - if (lineParts.length < 2) continue; - - [key] = lineParts; - if (entry == null) { - entry = { - sha: key, - originalLine: parseInt(lineParts[1], 10), - line: parseInt(lineParts[2], 10), - lineCount: parseInt(lineParts[3], 10), - } as unknown as BlameEntry; - - continue; - } +export function parseGitBlame( + container: Container, + data: string, + repoPath: string, + currentUser: GitUser | undefined, +): GitBlame | undefined { + using sw = maybeStopWatch(`Git.parseBlame(${repoPath})`, { log: false, logLevel: 'debug' }); + if (!data) return undefined; + + const authors = new Map(); + const commits = new Map(); + const lines: GitCommitLine[] = []; + + let entry: BlameEntry | undefined = undefined; + let key: string; + let line: string; + let lineParts: string[]; + + for (line of getLines(data)) { + lineParts = line.split(' '); + if (lineParts.length < 2) continue; + + [key] = lineParts; + if (entry == null) { + entry = { + sha: key, + originalLine: parseInt(lineParts[1], 10), + line: parseInt(lineParts[2], 10), + lineCount: parseInt(lineParts[3], 10), + } as unknown as BlameEntry; + + continue; + } - switch (key) { - case 'author': - if (entry.sha === uncommitted) { - entry.author = 'You'; - } else { - entry.author = line.slice(key.length + 1).trim(); - } - break; + switch (key) { + case 'author': + if (entry.sha === uncommitted) { + entry.author = 'You'; + } else { + entry.author = line.slice(key.length + 1).trim(); + } + break; - case 'author-mail': { - if (entry.sha === uncommitted) { - entry.authorEmail = currentUser?.email; - continue; - } + case 'author-mail': { + if (entry.sha === uncommitted) { + entry.authorEmail = currentUser?.email; + continue; + } - entry.authorEmail = line.slice(key.length + 1).trim(); - const start = entry.authorEmail.indexOf('<'); - if (start >= 0) { - const end = entry.authorEmail.indexOf('>', start); - if (end > start) { - entry.authorEmail = entry.authorEmail.substring(start + 1, end); - } else { - entry.authorEmail = entry.authorEmail.substring(start + 1); - } + entry.authorEmail = line.slice(key.length + 1).trim(); + const start = entry.authorEmail.indexOf('<'); + if (start >= 0) { + const end = entry.authorEmail.indexOf('>', start); + if (end > start) { + entry.authorEmail = entry.authorEmail.substring(start + 1, end); + } else { + entry.authorEmail = entry.authorEmail.substring(start + 1); } + } - break; + break; + } + case 'author-time': + entry.authorDate = lineParts[1]; + break; + + case 'author-tz': + entry.authorTimeZone = lineParts[1]; + break; + + case 'committer': + if (isUncommitted(entry.sha)) { + entry.committer = 'You'; + } else { + entry.committer = line.slice(key.length + 1).trim(); } - case 'author-time': - entry.authorDate = lineParts[1]; - break; + break; - case 'author-tz': - entry.authorTimeZone = lineParts[1]; - break; + case 'committer-mail': { + if (isUncommitted(entry.sha)) { + entry.committerEmail = currentUser?.email; + continue; + } - case 'committer': - if (isUncommitted(entry.sha)) { - entry.committer = 'You'; + entry.committerEmail = line.slice(key.length + 1).trim(); + const start = entry.committerEmail.indexOf('<'); + if (start >= 0) { + const end = entry.committerEmail.indexOf('>', start); + if (end > start) { + entry.committerEmail = entry.committerEmail.substring(start + 1, end); } else { - entry.committer = line.slice(key.length + 1).trim(); - } - break; - - case 'committer-mail': { - if (isUncommitted(entry.sha)) { - entry.committerEmail = currentUser?.email; - continue; + entry.committerEmail = entry.committerEmail.substring(start + 1); } - - entry.committerEmail = line.slice(key.length + 1).trim(); - const start = entry.committerEmail.indexOf('<'); - if (start >= 0) { - const end = entry.committerEmail.indexOf('>', start); - if (end > start) { - entry.committerEmail = entry.committerEmail.substring(start + 1, end); - } else { - entry.committerEmail = entry.committerEmail.substring(start + 1); - } - } - - break; } - case 'committer-time': - entry.committerDate = lineParts[1]; - break; - case 'committer-tz': - entry.committerTimeZone = lineParts[1]; - break; + break; + } + case 'committer-time': + entry.committerDate = lineParts[1]; + break; + + case 'committer-tz': + entry.committerTimeZone = lineParts[1]; + break; - case 'summary': - entry.summary = line.slice(key.length + 1).trim(); - break; + case 'summary': + entry.summary = line.slice(key.length + 1).trim(); + break; - case 'previous': - entry.previousSha = lineParts[1]; - entry.previousPath = lineParts.slice(2).join(' '); - break; + case 'previous': + entry.previousSha = lineParts[1]; + entry.previousPath = lineParts.slice(2).join(' '); + break; - case 'filename': - // Don't trim to allow spaces in the filename - entry.path = line.slice(key.length + 1); + case 'filename': + // Don't trim to allow spaces in the filename + entry.path = line.slice(key.length + 1); - // Since the filename marks the end of a commit, parse the entry and clear it for the next - GitBlameParser.parseEntry(container, entry, repoPath, commits, authors, lines, currentUser); + // Since the filename marks the end of a commit, parse the entry and clear it for the next + parseBlameEntry(container, entry, repoPath, commits, authors, lines, currentUser); - entry = undefined; - break; + entry = undefined; + break; - default: - break; - } + default: + break; } + } - for (const [, c] of commits) { - if (!c.author.name) continue; + for (const [, c] of commits) { + if (!c.author.name) continue; - const author = authors.get(c.author.name); - if (author == undefined) return undefined; + const author = authors.get(c.author.name); + if (author == undefined) return undefined; - author.lineCount += c.lines.length; - } + author.lineCount += c.lines.length; + } - const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); + const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); - const blame: GitBlame = { - repoPath: repoPath, - authors: sortedAuthors, - commits: commits, - lines: lines, - }; - return blame; - } + sw?.stop({ suffix: ` parsed ${lines.length} lines, ${commits.size} commits` }); - private static parseEntry( - container: Container, - entry: BlameEntry, - repoPath: string, - commits: Map, - authors: Map, - lines: GitCommitLine[], - currentUser: { name?: string; email?: string } | undefined, - ) { - let commit = commits.get(entry.sha); - if (commit == null) { - if (entry.author != null) { - if ( - currentUser != null && - // Name or e-mail is configured - (currentUser.name != null || currentUser.email != null) && - // Match on name if configured - (currentUser.name == null || currentUser.name === entry.author) && - // Match on email if configured - (currentUser.email == null || currentUser.email === entry.authorEmail) - ) { - entry.author = 'You'; - } + const blame: GitBlame = { + repoPath: repoPath, + authors: sortedAuthors, + commits: commits, + lines: lines, + }; + return blame; +} - let author = authors.get(entry.author); - if (author == null) { - author = { - name: entry.author, - lineCount: 0, - }; - authors.set(entry.author, author); - } +function parseBlameEntry( + container: Container, + entry: BlameEntry, + repoPath: string, + commits: Map, + authors: Map, + lines: GitCommitLine[], + currentUser: { name?: string; email?: string } | undefined, +) { + let commit = commits.get(entry.sha); + if (commit == null) { + if (entry.author != null) { + if ( + currentUser != null && + // Name or e-mail is configured + (currentUser.name != null || currentUser.email != null) && + // Match on name if configured + (currentUser.name == null || currentUser.name === entry.author) && + // Match on email if configured + (currentUser.email == null || currentUser.email === entry.authorEmail) + ) { + entry.author = 'You'; } - commit = new GitCommit( - container, - repoPath, - entry.sha, - new GitCommitIdentity(entry.author, entry.authorEmail, new Date((entry.authorDate as any) * 1000)), - new GitCommitIdentity( - entry.committer, - entry.committerEmail, - new Date((entry.committerDate as any) * 1000), - ), - entry.summary!, - [], - undefined, - new GitFileChange( - repoPath, - entry.path, - GitFileIndexStatus.Modified, - entry.previousPath && entry.previousPath !== entry.path ? entry.previousPath : undefined, - entry.previousSha, - ), - undefined, - [], - ); - - commits.set(entry.sha, commit); + let author = authors.get(entry.author); + if (author == null) { + author = { + name: entry.author, + lineCount: 0, + }; + authors.set(entry.author, author); + } } - for (let i = 0, len = entry.lineCount; i < len; i++) { - const line: GitCommitLine = { - sha: entry.sha, - previousSha: commit.file!.previousSha, - originalLine: entry.originalLine + i, - line: entry.line + i, - }; + commit = new GitCommit( + container, + repoPath, + entry.sha, + new GitCommitIdentity(entry.author, entry.authorEmail, new Date((entry.authorDate as any) * 1000)), + new GitCommitIdentity(entry.committer, entry.committerEmail, new Date((entry.committerDate as any) * 1000)), + entry.summary!, + [], + undefined, + new GitFileChange( + repoPath, + entry.path, + GitFileIndexStatus.Modified, + entry.previousPath && entry.previousPath !== entry.path ? entry.previousPath : undefined, + entry.previousSha, + ), + undefined, + [], + ); + + commits.set(entry.sha, commit); + } - commit.lines.push(line); - lines[line.line - 1] = line; - } + for (let i = 0, len = entry.lineCount; i < len; i++) { + const line: GitCommitLine = { + sha: entry.sha, + previousSha: commit.file!.previousSha, + originalLine: entry.originalLine + i, + line: entry.line + i, + }; + + commit.lines.push(line); + lines[line.line - 1] = line; } } diff --git a/src/git/parsers/branchParser.ts b/src/git/parsers/branchParser.ts index 96bc889..ffb1199 100644 --- a/src/git/parsers/branchParser.ts +++ b/src/git/parsers/branchParser.ts @@ -1,5 +1,5 @@ import type { Container } from '../../container'; -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import { GitBranch } from '../models/branch'; const branchWithTrackingRegex = @@ -9,73 +9,72 @@ const branchWithTrackingRegex = const lb = '%3c'; // `%${'<'.charCodeAt(0).toString(16)}`; const rb = '%3e'; // `%${'>'.charCodeAt(0).toString(16)}`; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitBranchParser { - static defaultFormat = [ - `${lb}h${rb}%(HEAD)`, // HEAD indicator - `${lb}n${rb}%(refname)`, // branch name - `${lb}u${rb}%(upstream:short)`, // branch upstream - `${lb}t${rb}%(upstream:track)`, // branch upstream tracking state - `${lb}r${rb}%(objectname)`, // ref - `${lb}d${rb}%(committerdate:iso8601)`, // committer date - ].join(''); +export const parseGitBranchesDefaultFormat = [ + `${lb}h${rb}%(HEAD)`, // HEAD indicator + `${lb}n${rb}%(refname)`, // branch name + `${lb}u${rb}%(upstream:short)`, // branch upstream + `${lb}t${rb}%(upstream:track)`, // branch upstream tracking state + `${lb}r${rb}%(objectname)`, // ref + `${lb}d${rb}%(committerdate:iso8601)`, // committer date +].join(''); - @debug({ args: false, singleLine: true }) - static parse(container: Container, data: string, repoPath: string): GitBranch[] { - const branches: GitBranch[] = []; +export function parseGitBranches(container: Container, data: string, repoPath: string): GitBranch[] { + using sw = maybeStopWatch(`Git.parseBranches(${repoPath})`, { log: false, logLevel: 'debug' }); - if (!data) return branches; + const branches: GitBranch[] = []; + if (!data) return branches; - let current; - let name; - let upstream; - let ahead; - let behind; - let missing; - let ref; - let date; + let current; + let name; + let upstream; + let ahead; + let behind; + let missing; + let ref; + let date; - let remote; + let remote; - let match; - do { - match = branchWithTrackingRegex.exec(data); - if (match == null) break; + let match; + do { + match = branchWithTrackingRegex.exec(data); + if (match == null) break; - [, current, name, upstream, ahead, behind, missing, ref, date] = match; + [, current, name, upstream, ahead, behind, missing, ref, date] = match; - if (name.startsWith('refs/remotes/')) { - // Strip off refs/remotes/ - name = name.substr(13); - if (name.endsWith('/HEAD')) continue; + if (name.startsWith('refs/remotes/')) { + // Strip off refs/remotes/ + name = name.substr(13); + if (name.endsWith('/HEAD')) continue; - remote = true; - } else { - // Strip off refs/heads/ - name = name.substr(11); - remote = false; - } + remote = true; + } else { + // Strip off refs/heads/ + name = name.substr(11); + remote = false; + } - branches.push( - new GitBranch( - container, - repoPath, - name, - remote, - current.charCodeAt(0) === 42, // '*', - date ? new Date(date) : undefined, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ref == null || ref.length === 0 ? undefined : ` ${ref}`.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - upstream == null || upstream.length === 0 - ? undefined - : { name: ` ${upstream}`.substr(1), missing: Boolean(missing) }, - Number(ahead) || 0, - Number(behind) || 0, - ), - ); - } while (true); + branches.push( + new GitBranch( + container, + repoPath, + name, + remote, + current.charCodeAt(0) === 42, // '*', + date ? new Date(date) : undefined, + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ref == null || ref.length === 0 ? undefined : ` ${ref}`.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + upstream == null || upstream.length === 0 + ? undefined + : { name: ` ${upstream}`.substr(1), missing: Boolean(missing) }, + Number(ahead) || 0, + Number(behind) || 0, + ), + ); + } while (true); - return branches; - } + sw?.stop({ suffix: ` parsed ${branches.length} branches` }); + + return branches; } diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index 915a3aa..1238414 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -1,5 +1,5 @@ import { maybeStopWatch } from '../../system/stopwatch'; -import { getLines, pluralize } from '../../system/string'; +import { getLines } from '../../system/string'; import type { GitDiffFile, GitDiffHunkLine, GitDiffLine, GitDiffShortStat } from '../models/diff'; import { GitDiffHunk } from '../models/diff'; import type { GitFile, GitFileStatus } from '../models/file'; @@ -7,11 +7,10 @@ import type { GitFile, GitFileStatus } from '../models/file'; const shortStatDiffRegex = /(\d+)\s+files? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/; const unifiedDiffRegex = /^@@ -([\d]+)(?:,([\d]+))? \+([\d]+)(?:,([\d]+))? @@(?:.*?)\n([\s\S]*?)(?=^@@)/gm; -export function parseFileDiff(data: string, includeContents: boolean = false): GitDiffFile | undefined { +export function parseGitFileDiff(data: string, includeContents: boolean = false): GitDiffFile | undefined { + using sw = maybeStopWatch('Git.parseFileDiff', { log: false, logLevel: 'debug' }); if (!data) return undefined; - const sw = maybeStopWatch('parseFileDiff', { log: false, logLevel: 'debug' }); - const hunks: GitDiffHunk[] = []; let previousStart; @@ -54,7 +53,7 @@ export function parseFileDiff(data: string, includeContents: boolean = false): G ); } while (true); - sw?.stop({ suffix: ` parsed ${pluralize('hunk', hunks.length)}` }); + sw?.stop({ suffix: ` parsed ${hunks.length} hunks` }); if (!hunks.length) return undefined; @@ -65,8 +64,11 @@ export function parseFileDiff(data: string, includeContents: boolean = false): G return diff; } -export function parseDiffHunk(hunk: GitDiffHunk): { lines: GitDiffHunkLine[]; state: 'added' | 'changed' | 'removed' } { - const sw = maybeStopWatch('parseDiffHunk', { log: false, logLevel: 'debug' }); +export function parseGitDiffHunk(hunk: GitDiffHunk): { + lines: GitDiffHunkLine[]; + state: 'added' | 'changed' | 'removed'; +} { + using sw = maybeStopWatch('Git.parseDiffHunk', { log: false, logLevel: 'debug' }); const currentStart = hunk.current.position.start; const previousStart = hunk.previous.position.start; @@ -140,7 +142,7 @@ export function parseDiffHunk(hunk: GitDiffHunk): { lines: GitDiffHunkLine[]; st }); } - sw?.stop({ suffix: ` parsed ${pluralize('line', hunkLines.length)}` }); + sw?.stop({ suffix: ` parsed ${hunkLines.length} hunk lines` }); return { lines: hunkLines, @@ -148,11 +150,10 @@ export function parseDiffHunk(hunk: GitDiffHunk): { lines: GitDiffHunkLine[]; st }; } -export function parseDiffNameStatusFiles(data: string, repoPath: string): GitFile[] | undefined { +export function parseGitDiffNameStatusFiles(data: string, repoPath: string): GitFile[] | undefined { + using sw = maybeStopWatch('Git.parseDiffNameStatusFiles', { log: false, logLevel: 'debug' }); if (!data) return undefined; - const sw = maybeStopWatch('parseDiffNameStatusFiles', { log: false, logLevel: 'debug' }); - const files: GitFile[] = []; let status; @@ -172,16 +173,15 @@ export function parseDiffNameStatusFiles(data: string, repoPath: string): GitFil }); } - sw?.stop({ suffix: ` parsed ${pluralize('file', files.length)}` }); + sw?.stop({ suffix: ` parsed ${files.length} files` }); return files; } -export function parseDiffShortStat(data: string): GitDiffShortStat | undefined { +export function parseGitDiffShortStat(data: string): GitDiffShortStat | undefined { + using sw = maybeStopWatch('Git.parseDiffShortStat', { log: false, logLevel: 'debug' }); if (!data) return undefined; - const sw = maybeStopWatch('parseDiffShortStat', { log: false, logLevel: 'debug' }); - const match = shortStatDiffRegex.exec(data); if (match == null) return undefined; @@ -194,9 +194,7 @@ export function parseDiffShortStat(data: string): GitDiffShortStat | undefined { }; sw?.stop({ - suffix: ` parsed ${pluralize('file', diffShortStat.changedFiles)}, +${diffShortStat.additions} -${ - diffShortStat.deletions - }`, + suffix: ` parsed ${diffShortStat.changedFiles} files, +${diffShortStat.additions} -${diffShortStat.deletions}`, }); return diffShortStat; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index c47af41..2a57321 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -1,8 +1,8 @@ import type { Range } from 'vscode'; import type { Container } from '../../container'; import { filterMap } from '../../system/array'; -import { debug } from '../../system/decorators/log'; import { normalizePath, relative } from '../../system/path'; +import { maybeStopWatch } from '../../system/stopwatch'; import { getLines } from '../../system/string'; import type { GitCommitLine, GitStashCommit } from '../models/commit'; import { GitCommit, GitCommitIdentity } from '../models/commit'; @@ -363,581 +363,548 @@ export function createLogParserWithStats>( }); } -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitLogParser { - // 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}`, - `${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 - `${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}s${rb}`, - '%B', // summary - `${lb}${sl}s${rb}`, - `${lb}f${rb}`, - ].join('%n'); - - static simpleRefs = `${lb}r${rb}${sp}%H`; - static simpleFormat = `${lb}r${rb}${sp}%H`; - - static shortlog = '%H%x00%aN%x00%aE%x00%at'; - - @debug({ args: false }) - static parse( - container: Container, - data: string, - type: LogType, - repoPath: string | undefined, - fileName: string | undefined, - sha: string | undefined, - currentUser: GitUser | undefined, - limit: number | undefined, - reverse: boolean, - range: Range | undefined, - stashes?: Map, - hasMoreOverride?: boolean, - ): GitLog | undefined { - if (!data) return undefined; - - let relativeFileName: string | undefined; - - let entry: LogEntry = {}; - let line: string | undefined = undefined; - let token: number; - - let i = 0; - let first = true; - - const lines = getLines(`${data}`); - // Skip the first line since it will always be - let next = lines.next(); - if (next.done) return undefined; - - if (repoPath !== undefined) { - repoPath = normalizePath(repoPath); - } +export const parseGitLogAllFormat = [ + `${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'); +export const parseGitLogDefaultFormat = [ + `${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}s${rb}`, + '%B', // summary + `${lb}${sl}s${rb}`, + `${lb}f${rb}`, +].join('%n'); +export const parseGitLogSimpleFormat = `${lb}r${rb}${sp}%H`; + +export function parseGitLog( + container: Container, + data: string, + type: LogType, + repoPath: string | undefined, + fileName: string | undefined, + sha: string | undefined, + currentUser: GitUser | undefined, + limit: number | undefined, + reverse: boolean, + range: Range | undefined, + stashes?: Map, + hasMoreOverride?: boolean, +): GitLog | undefined { + using sw = maybeStopWatch(`Git.parseLog(${repoPath}, fileName=${fileName}, sha=${sha})`, { + log: false, + logLevel: 'debug', + }); + if (!data) return undefined; - const commits = new Map(); - let truncationCount = limit; + let relativeFileName: string | undefined; - let match; - let renamedFileName; - let renamedMatch; + let entry: LogEntry = {}; + let line: string | undefined = undefined; + let token: number; - loop: while (true) { - next = lines.next(); - if (next.done) break; + let i = 0; + let first = true; - line = next.value; + const lines = getLines(`${data}`); + // Skip the first line since it will always be + let next = lines.next(); + if (next.done) return undefined; - // Since log --reverse doesn't properly honor a max count -- enforce it here - if (reverse && limit && i >= limit) break; + if (repoPath !== undefined) { + repoPath = normalizePath(repoPath); + } - // <1-char token> data - // e.g. bd1452a2dc - token = line.charCodeAt(1); + const commits = new Map(); + let truncationCount = limit; - switch (token) { - case 114: // 'r': // ref - entry = { - sha: line.substring(4), - }; - break; + let match; + let renamedFileName; + let renamedMatch; - case 97: // 'a': // author - if (uncommitted === entry.sha) { - entry.author = 'You'; - } else { - entry.author = line.substring(4); - } - break; + loop: while (true) { + next = lines.next(); + if (next.done) break; - case 101: // 'e': // author-mail - entry.authorEmail = line.substring(4); - break; + line = next.value; - case 100: // 'd': // author-date - entry.authorDate = line.substring(4); - break; + // Since log --reverse doesn't properly honor a max count -- enforce it here + if (reverse && limit && i >= limit) break; - case 110: // 'n': // committer - entry.committer = line.substring(4); - break; + // <1-char token> data + // e.g. bd1452a2dc + token = line.charCodeAt(1); - case 109: // 'm': // committer-mail - entry.committedDate = line.substring(4); - break; + switch (token) { + case 114: // 'r': // ref + entry = { + sha: line.substring(4), + }; + break; - case 99: // 'c': // committer-date - entry.committedDate = line.substring(4); - break; + case 97: // 'a': // author + if (uncommitted === entry.sha) { + entry.author = 'You'; + } else { + entry.author = line.substring(4); + } + break; - case 112: // 'p': // parents - line = line.substring(4); - entry.parentShas = line.length !== 0 ? line.split(' ') : undefined; - break; + case 101: // 'e': // author-mail + entry.authorEmail = line.substring(4); + break; - case 116: // 't': // tips - line = line.substring(4); - entry.tips = line.length !== 0 ? line.split(', ') : undefined; - break; + case 100: // 'd': // author-date + entry.authorDate = line.substring(4); + break; - case 115: // 's': // summary - while (true) { - next = lines.next(); - if (next.done) break; + case 110: // 'n': // committer + entry.committer = line.substring(4); + break; - line = next.value; - if (line === '') break; + case 109: // 'm': // committer-mail + entry.committedDate = line.substring(4); + break; - if (entry.summary === undefined) { - entry.summary = line; - } else { - entry.summary += `\n${line}`; - } - } + case 99: // 'c': // committer-date + entry.committedDate = line.substring(4); + break; - // Remove the trailing newline - if (entry.summary != null && entry.summary.charCodeAt(entry.summary.length - 1) === 10) { - entry.summary = entry.summary.slice(0, -1); - } - break; + case 112: // 'p': // parents + 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 102: { - // 'f': // files - // Skip the blank line git adds before the files + case 115: // 's': // summary + while (true) { next = lines.next(); + if (next.done) break; - let hasFiles = true; - if (next.done || next.value === '') { - // If this is a merge commit and there are no files returned, skip the commit and reduce our truncationCount to ensure accurate truncation detection - if ((entry.parentShas?.length ?? 0) > 1) { - if (truncationCount) { - truncationCount--; - } + line = next.value; + if (line === '') break; - break; + if (entry.summary === undefined) { + entry.summary = line; + } else { + entry.summary += `\n${line}`; + } + } + + // Remove the trailing newline + if (entry.summary != null && entry.summary.charCodeAt(entry.summary.length - 1) === 10) { + entry.summary = entry.summary.slice(0, -1); + } + break; + + case 102: { + // 'f': // files + // Skip the blank line git adds before the files + next = lines.next(); + + let hasFiles = true; + if (next.done || next.value === '') { + // If this is a merge commit and there are no files returned, skip the commit and reduce our truncationCount to ensure accurate truncation detection + if ((entry.parentShas?.length ?? 0) > 1) { + if (truncationCount) { + truncationCount--; } - hasFiles = false; + break; } - // eslint-disable-next-line no-unmodified-loop-condition - while (hasFiles) { - next = lines.next(); - if (next.done) break; + hasFiles = false; + } - line = next.value; - if (line === '') break; + // eslint-disable-next-line no-unmodified-loop-condition + while (hasFiles) { + next = lines.next(); + if (next.done) break; - if (line.startsWith('warning:')) continue; + line = next.value; + if (line === '') break; - if (type === LogType.Log) { - match = fileStatusRegex.exec(line); - if (match != null) { - if (entry.files === undefined) { - entry.files = []; - } + if (line.startsWith('warning:')) continue; - renamedFileName = match[3]; - if (renamedFileName !== undefined) { - entry.files.push({ - status: match[1] as GitFileIndexStatus, - path: renamedFileName, - originalPath: match[2], - }); - } else { - entry.files.push({ - status: match[1] as GitFileIndexStatus, - path: match[2], - }); - } + if (type === LogType.Log) { + match = fileStatusRegex.exec(line); + if (match != null) { + if (entry.files === undefined) { + entry.files = []; } - } else { - match = diffRegex.exec(line); - if (match != null) { - [, entry.originalPath, entry.path] = match; - if (entry.path === entry.originalPath) { - entry.originalPath = undefined; - entry.status = GitFileIndexStatus.Modified; - } else { - entry.status = GitFileIndexStatus.Renamed; - } - void lines.next(); - void lines.next(); - next = lines.next(); + renamedFileName = match[3]; + if (renamedFileName !== undefined) { + entry.files.push({ + status: match[1] as GitFileIndexStatus, + path: renamedFileName, + originalPath: match[2], + }); + } else { + entry.files.push({ + status: match[1] as GitFileIndexStatus, + path: match[2], + }); + } + } + } else { + match = diffRegex.exec(line); + if (match != null) { + [, entry.originalPath, entry.path] = match; + if (entry.path === entry.originalPath) { + entry.originalPath = undefined; + entry.status = GitFileIndexStatus.Modified; + } else { + entry.status = GitFileIndexStatus.Renamed; + } - match = diffRangeRegex.exec(next.value); - if (match !== null) { - entry.line = { - sha: entry.sha!, - originalLine: parseInt(match[1], 10), - // count: parseInt(match[2], 10), - line: parseInt(match[3], 10), - // count: parseInt(match[4], 10), - }; - } + void lines.next(); + void lines.next(); + next = lines.next(); + + match = diffRangeRegex.exec(next.value); + if (match !== null) { + entry.line = { + sha: entry.sha!, + originalLine: parseInt(match[1], 10), + // count: parseInt(match[2], 10), + line: parseInt(match[3], 10), + // count: parseInt(match[4], 10), + }; + } - while (true) { - next = lines.next(); - if (next.done || next.value === '') break; - } - break; - } else { + while (true) { next = lines.next(); - match = fileStatusAndSummaryRegex.exec(`${line}\n${next.value}`); - if (match != null) { - entry.fileStats = { - additions: Number(match[1]) || 0, - deletions: Number(match[2]) || 0, - changes: 0, - }; - - switch (match[4]) { - case undefined: - entry.status = 'M' as GitFileIndexStatus; - entry.path = match[3]; - break; - case 'copy': - case 'rename': - entry.status = (match[4] === 'copy' ? 'C' : 'R') as GitFileIndexStatus; - - renamedFileName = match[3]; - renamedMatch = - fileStatusAndSummaryRenamedFilePathRegex.exec(renamedFileName); - if (renamedMatch != null) { - const [, start, from, to, end] = renamedMatch; - // If there is no new path, the path part was removed so ensure we don't end up with // - if (!to) { - entry.path = `${ - start.endsWith('/') && end.startsWith('/') - ? start.slice(0, -1) - : start - }${end}`; - } else { - entry.path = `${start}${to}${end}`; - } - - if (!from) { - entry.originalPath = `${ - start.endsWith('/') && end.startsWith('/') - ? start.slice(0, -1) - : start - }${end}`; - } else { - entry.originalPath = `${start}${from}${end}`; - } + if (next.done || next.value === '') break; + } + break; + } else { + next = lines.next(); + match = fileStatusAndSummaryRegex.exec(`${line}\n${next.value}`); + if (match != null) { + entry.fileStats = { + additions: Number(match[1]) || 0, + deletions: Number(match[2]) || 0, + changes: 0, + }; + + switch (match[4]) { + case undefined: + entry.status = 'M' as GitFileIndexStatus; + entry.path = match[3]; + break; + case 'copy': + case 'rename': + entry.status = (match[4] === 'copy' ? 'C' : 'R') as GitFileIndexStatus; + + renamedFileName = match[3]; + renamedMatch = fileStatusAndSummaryRenamedFilePathRegex.exec(renamedFileName); + if (renamedMatch != null) { + const [, start, from, to, end] = renamedMatch; + // If there is no new path, the path part was removed so ensure we don't end up with // + if (!to) { + entry.path = `${ + start.endsWith('/') && end.startsWith('/') + ? start.slice(0, -1) + : start + }${end}`; } else { - renamedMatch = - fileStatusAndSummaryRenamedFileRegex.exec(renamedFileName); - if (renamedMatch != null) { - entry.path = renamedMatch[2]; - entry.originalPath = renamedMatch[1]; - } else { - entry.path = renamedFileName; - } + entry.path = `${start}${to}${end}`; } - break; - case 'create': - entry.status = 'A' as GitFileIndexStatus; - entry.path = match[3]; - break; - case 'delete': - entry.status = 'D' as GitFileIndexStatus; - entry.path = match[3]; - break; - default: - entry.status = 'M' as GitFileIndexStatus; - entry.path = match[3]; - break; - } + if (!from) { + entry.originalPath = `${ + start.endsWith('/') && end.startsWith('/') + ? start.slice(0, -1) + : start + }${end}`; + } else { + entry.originalPath = `${start}${from}${end}`; + } + } else { + renamedMatch = fileStatusAndSummaryRenamedFileRegex.exec(renamedFileName); + if (renamedMatch != null) { + entry.path = renamedMatch[2]; + entry.originalPath = renamedMatch[1]; + } else { + entry.path = renamedFileName; + } + } + + break; + case 'create': + entry.status = 'A' as GitFileIndexStatus; + entry.path = match[3]; + break; + case 'delete': + entry.status = 'D' as GitFileIndexStatus; + entry.path = match[3]; + break; + default: + entry.status = 'M' as GitFileIndexStatus; + entry.path = match[3]; + break; } - - if (next.done || next.value === '') break; } - } - } - if (entry.files !== undefined) { - entry.path = filterMap(entry.files, f => (f.path ? f.path : undefined)).join(', '); + if (next.done || next.value === '') break; + } } + } - if (first && repoPath === undefined && type === LogType.LogFile && fileName !== undefined) { - // Try to get the repoPath from the most recent commit - repoPath = normalizePath( - fileName.replace(fileName.startsWith('/') ? `/${entry.path}` : entry.path!, ''), - ); - relativeFileName = normalizePath(relative(repoPath, fileName)); - } else { - relativeFileName = - entry.path ?? - (repoPath != null && fileName != null - ? normalizePath(relative(repoPath, fileName)) - : undefined); - } - first = false; - - const commit = commits.get(entry.sha!); - if (commit === undefined) { - i++; - if (limit && i > limit) break loop; - } else if (truncationCount) { - // Since this matches an existing commit it will be skipped, so reduce our truncationCount to ensure accurate truncation detection - truncationCount--; - } + if (entry.files !== undefined) { + entry.path = filterMap(entry.files, f => (f.path ? f.path : undefined)).join(', '); + } - GitLogParser.parseEntry( - container, - entry, - commit, - type, - repoPath, - relativeFileName, - commits, - currentUser, - stashes, + if (first && repoPath === undefined && type === LogType.LogFile && fileName !== undefined) { + // Try to get the repoPath from the most recent commit + repoPath = normalizePath( + fileName.replace(fileName.startsWith('/') ? `/${entry.path}` : entry.path!, ''), ); - - break; + relativeFileName = normalizePath(relative(repoPath, fileName)); + } else { + relativeFileName = + entry.path ?? + (repoPath != null && fileName != null + ? normalizePath(relative(repoPath, fileName)) + : undefined); } + first = false; + + const commit = commits.get(entry.sha!); + if (commit === undefined) { + i++; + if (limit && i > limit) break loop; + } else if (truncationCount) { + // Since this matches an existing commit it will be skipped, so reduce our truncationCount to ensure accurate truncation detection + truncationCount--; + } + + parseLogEntry( + container, + entry, + commit, + type, + repoPath, + relativeFileName, + commits, + currentUser, + stashes, + ); + + break; } } - - const log: GitLog = { - repoPath: repoPath!, - commits: commits, - sha: sha, - count: i, - limit: limit, - range: range, - hasMore: hasMoreOverride ?? Boolean(truncationCount && i > truncationCount && truncationCount !== 1), - }; - return log; } - private static parseEntry( - container: Container, - entry: LogEntry, - commit: GitCommit | undefined, - type: LogType, - repoPath: string | undefined, - relativeFileName: string | undefined, - commits: Map, - currentUser: GitUser | undefined, - stashes: Map | undefined, - ): void { - if (commit == null) { - if (entry.author != null) { - if (isUserMatch(currentUser, entry.author, entry.authorEmail)) { - entry.author = 'You'; - } + sw?.stop({ suffix: ` parsed ${commits.size} commits` }); + + const log: GitLog = { + repoPath: repoPath!, + commits: commits, + sha: sha, + count: i, + limit: limit, + range: range, + hasMore: hasMoreOverride ?? Boolean(truncationCount && i > truncationCount && truncationCount !== 1), + }; + return log; +} + +function parseLogEntry( + container: Container, + entry: LogEntry, + commit: GitCommit | undefined, + type: LogType, + repoPath: string | undefined, + relativeFileName: string | undefined, + commits: Map, + currentUser: GitUser | undefined, + stashes: Map | undefined, +): void { + if (commit == null) { + if (entry.author != null) { + if (isUserMatch(currentUser, entry.author, entry.authorEmail)) { + entry.author = 'You'; } + } - if (entry.committer != null) { - if (isUserMatch(currentUser, entry.committer, entry.committerEmail)) { - entry.committer = 'You'; - } + if (entry.committer != null) { + if (isUserMatch(currentUser, entry.committer, entry.committerEmail)) { + entry.committer = 'You'; } + } - const originalFileName = entry.originalPath ?? (relativeFileName !== entry.path ? entry.path : undefined); + const originalFileName = entry.originalPath ?? (relativeFileName !== entry.path ? entry.path : undefined); - const files: { file?: GitFileChange; files?: GitFileChange[] } = { - files: entry.files?.map(f => new GitFileChange(repoPath!, f.path, f.status, f.originalPath)), - }; - if (type === LogType.LogFile && relativeFileName != null) { - files.file = new GitFileChange( - repoPath!, - relativeFileName, - entry.status!, - originalFileName, - undefined, - entry.fileStats, - ); - } + const files: { file?: GitFileChange; files?: GitFileChange[] } = { + files: entry.files?.map(f => new GitFileChange(repoPath!, f.path, f.status, f.originalPath)), + }; + if (type === LogType.LogFile && relativeFileName != null) { + files.file = new GitFileChange( + repoPath!, + relativeFileName, + entry.status!, + originalFileName, + undefined, + entry.fileStats, + ); + } - const stash = stashes?.get(entry.sha!); - if (stash != null) { - commit = new GitCommit( - container, - repoPath!, - stash.sha, - stash.author, - stash.committer, - stash.summary, - stash.parents, - stash.message, - files, - undefined, - entry.line != null ? [entry.line] : [], - entry.tips, - stash.stashName, - stash.stashOnRef, - ); - commits.set(stash.sha, commit); - } else { - commit = new GitCommit( - container, - repoPath!, - entry.sha!, - - new GitCommitIdentity( - entry.author!, - entry.authorEmail, - new Date((entry.authorDate! as any) * 1000), - ), - - new GitCommitIdentity( - entry.committer!, - entry.committerEmail, - new Date((entry.committedDate! as any) * 1000), - ), - entry.summary?.split('\n', 1)[0] ?? '', - entry.parentShas ?? [], - entry.summary ?? '', - files, - undefined, - entry.line != null ? [entry.line] : [], - entry.tips, - ); - commits.set(entry.sha!, commit); - } + const stash = stashes?.get(entry.sha!); + if (stash != null) { + commit = new GitCommit( + container, + repoPath!, + stash.sha, + stash.author, + stash.committer, + stash.summary, + stash.parents, + stash.message, + files, + undefined, + entry.line != null ? [entry.line] : [], + entry.tips, + stash.stashName, + stash.stashOnRef, + ); + commits.set(stash.sha, commit); + } else { + commit = new GitCommit( + container, + repoPath!, + entry.sha!, + + new GitCommitIdentity(entry.author!, entry.authorEmail, new Date((entry.authorDate! as any) * 1000)), + + new GitCommitIdentity( + entry.committer!, + entry.committerEmail, + new Date((entry.committedDate! as any) * 1000), + ), + entry.summary?.split('\n', 1)[0] ?? '', + entry.parentShas ?? [], + entry.summary ?? '', + files, + undefined, + entry.line != null ? [entry.line] : [], + entry.tips, + ); + commits.set(entry.sha!, commit); } } +} + +export function parseGitLogSimple( + data: string, + skip: number, + skipRef?: string, +): [string | undefined, string | undefined, GitFileIndexStatus | undefined] { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + using _sw = maybeStopWatch('Git.parseLogSimple', { log: false, logLevel: 'debug' }); + + let ref; + let diffFile; + let diffRenamed; + let status; + let file; + let renamed; - @debug({ args: false }) - static parseSimple( - data: string, - skip: number, - skipRef?: string, - ): [string | undefined, string | undefined, GitFileIndexStatus | undefined] { - let ref; - let diffFile; - let diffRenamed; - let status; - let file; - let renamed; - - let match; - do { - match = logFileSimpleRegex.exec(data); - if (match == null) break; - - if (match[1] === skipRef) continue; - if (skip-- > 0) continue; - - [, ref, diffFile, diffRenamed, status, file, renamed] = match; - - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - file = ` ${diffRenamed || diffFile || renamed || file}`.substr(1); - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - status = status == null || status.length === 0 ? undefined : ` ${status}`.substr(1); - } while (skip >= 0); - - // Ensure the regex state is reset - logFileSimpleRegex.lastIndex = 0; + let match; + do { + match = logFileSimpleRegex.exec(data); + if (match == null) break; + + if (match[1] === skipRef) continue; + if (skip-- > 0) continue; + + [, ref, diffFile, diffRenamed, status, file, renamed] = match; // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - return [ - ref == null || ref.length === 0 ? undefined : ` ${ref}`.substr(1), - file, - status as GitFileIndexStatus | undefined, - ]; - } + file = ` ${diffRenamed || diffFile || renamed || file}`.substr(1); + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + status = status == null || status.length === 0 ? undefined : ` ${status}`.substr(1); + } while (skip >= 0); + + // Ensure the regex state is reset + logFileSimpleRegex.lastIndex = 0; + + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + return [ + ref == null || ref.length === 0 ? undefined : ` ${ref}`.substr(1), + file, + status as GitFileIndexStatus | undefined, + ]; +} - @debug({ args: false }) - static parseSimpleRenamed( - data: string, - originalFileName: string, - ): [string | undefined, string | undefined, GitFileIndexStatus | undefined] { - let match = logFileSimpleRenamedRegex.exec(data); - if (match == null) return [undefined, undefined, undefined]; +export function parseGitLogSimpleRenamed( + data: string, + originalFileName: string, +): [string | undefined, string | undefined, GitFileIndexStatus | undefined] { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + using _sw = maybeStopWatch('Git.parseLogSimpleRenamed', { log: false, logLevel: 'debug' }); - const [, ref, files] = match; + let match = logFileSimpleRenamedRegex.exec(data); + if (match == null) return [undefined, undefined, undefined]; - let status; - let file; - let renamed; + const [, ref, files] = match; - do { - match = logFileSimpleRenamedFilesRegex.exec(files); - if (match == null) break; + let status; + let file; + let renamed; - [, status, file, renamed] = match; + do { + match = logFileSimpleRenamedFilesRegex.exec(files); + if (match == null) break; - if (originalFileName !== file) { - status = undefined; - file = undefined; - renamed = undefined; - continue; - } + [, status, file, renamed] = match; - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - file = ` ${renamed || file}`.substr(1); - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - status = status == null || status.length === 0 ? undefined : ` ${status}`.substr(1); + if (originalFileName !== file) { + status = undefined; + file = undefined; + renamed = undefined; + continue; + } + + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + file = ` ${renamed || file}`.substr(1); + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + status = status == null || status.length === 0 ? undefined : ` ${status}`.substr(1); - break; - } while (true); + break; + } while (true); - // Ensure the regex state is reset - logFileSimpleRenamedFilesRegex.lastIndex = 0; + // Ensure the regex state is reset + logFileSimpleRenamedFilesRegex.lastIndex = 0; - return [ - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ref == null || ref.length === 0 || file == null ? undefined : ` ${ref}`.substr(1), - file, - status as GitFileIndexStatus | undefined, - ]; - } + return [ + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ref == null || ref.length === 0 || file == null ? undefined : ` ${ref}`.substr(1), + file, + status as GitFileIndexStatus | undefined, + ]; } diff --git a/src/git/parsers/reflogParser.ts b/src/git/parsers/reflogParser.ts index 1bbcf49..3f56201 100644 --- a/src/git/parsers/reflogParser.ts +++ b/src/git/parsers/reflogParser.ts @@ -1,4 +1,4 @@ -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import type { GitReflog } from '../models/reflog'; import { GitReflogRecord } from '../models/reflog'; @@ -10,119 +10,118 @@ const reflogHEADRegex = /.*?\/?HEAD$/; const lb = '%x3c'; // `%x${'<'.charCodeAt(0).toString(16)}`; const rb = '%x3e'; // `%x${'>'.charCodeAt(0).toString(16)}`; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitReflogParser { - static defaultFormat = [ - `${lb}r${rb}%H`, // ref - `${lb}d${rb}%gD`, // reflog selector (with iso8601 timestamp) - `${lb}s${rb}%gs`, // reflog subject - // `${lb}n${rb}%D` // ref names - ].join(''); - - @debug({ args: false }) - static parse( - data: string, - repoPath: string, - commands: string[], - limit: number, - totalLimit: number, - ): GitReflog | undefined { - if (!data) return undefined; - - const records: GitReflogRecord[] = []; - - let sha; - let selector; - let date; - let command; - let commandArgs; - let details; - - let head; - let headDate; - let headSha; - - let count = 0; - let total = 0; - let recordDate; - let record: GitReflogRecord | undefined; - - let match; - do { - match = reflogRegex.exec(data); - if (match == null) break; - - [, sha, selector, date, command, commandArgs, details] = match; - - total++; - - if (record !== undefined) { - // If the next record has the same sha as the previous, use it if it is not pointing to just HEAD and the previous is +export const parseGitRefLogDefaultFormat = [ + `${lb}r${rb}%H`, // ref + `${lb}d${rb}%gD`, // reflog selector (with iso8601 timestamp) + `${lb}s${rb}%gs`, // reflog subject + // `${lb}n${rb}%D` // ref names +].join(''); + +export function parseGitRefLog( + data: string, + repoPath: string, + commands: string[], + limit: number, + totalLimit: number, +): GitReflog | undefined { + using sw = maybeStopWatch(`Git.parseRefLog(${repoPath})`, { log: false, logLevel: 'debug' }); + if (!data) return undefined; + + const records: GitReflogRecord[] = []; + + let sha; + let selector; + let date; + let command; + let commandArgs; + let details; + + let head; + let headDate; + let headSha; + + let count = 0; + let total = 0; + let recordDate; + let record: GitReflogRecord | undefined; + + let match; + do { + match = reflogRegex.exec(data); + if (match == null) break; + + [, sha, selector, date, command, commandArgs, details] = match; + + total++; + + if (record !== undefined) { + // If the next record has the same sha as the previous, use it if it is not pointing to just HEAD and the previous is + if ( + sha === record.sha && + (date !== recordDate || !reflogHEADRegex.test(record.selector) || reflogHEADRegex.test(selector)) + ) { + continue; + } + + if (sha !== record.sha) { if ( - sha === record.sha && - (date !== recordDate || !reflogHEADRegex.test(record.selector) || reflogHEADRegex.test(selector)) + head != null && + headDate === recordDate && + headSha == record.sha && + reflogHEADRegex.test(record.selector) ) { - continue; + record.update(sha, head); + } else { + record.update(sha); } - if (sha !== record.sha) { - if ( - head != null && - headDate === recordDate && - headSha == record.sha && - reflogHEADRegex.test(record.selector) - ) { - record.update(sha, head); - } else { - record.update(sha); - } - - records.push(record); - record = undefined; - recordDate = undefined; - - count++; - if (limit !== 0 && count >= limit) break; - } - } - - if (command === 'HEAD') { - head = selector; - headDate = date; - headSha = sha; - - continue; - } + records.push(record); + record = undefined; + recordDate = undefined; - if (commands.includes(command)) { - record = new GitReflogRecord( - repoPath, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${sha}`.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${selector}`.substr(1), - new Date(date), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${command}`.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - commandArgs == null || commandArgs.length === 0 ? undefined : commandArgs.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - details == null || details.length === 0 ? undefined : details.substr(1), - ); - recordDate = date; + count++; + if (limit !== 0 && count >= limit) break; } - } while (true); - - // Ensure the regex state is reset - reflogRegex.lastIndex = 0; - - return { - repoPath: repoPath, - records: records, - count: count, - total: total, - limit: limit, - hasMore: (limit !== 0 && count >= limit) || (totalLimit !== 0 && total >= totalLimit), - }; - } + } + + if (command === 'HEAD') { + head = selector; + headDate = date; + headSha = sha; + + continue; + } + + if (commands.includes(command)) { + record = new GitReflogRecord( + repoPath, + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${sha}`.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${selector}`.substr(1), + new Date(date), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${command}`.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + commandArgs == null || commandArgs.length === 0 ? undefined : commandArgs.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + details == null || details.length === 0 ? undefined : details.substr(1), + ); + recordDate = date; + } + } while (true); + + // Ensure the regex state is reset + reflogRegex.lastIndex = 0; + + sw?.stop({ suffix: ` parsed ${records.length} records` }); + + return { + repoPath: repoPath, + records: records, + count: count, + total: total, + limit: limit, + hasMore: (limit !== 0 && count >= limit) || (totalLimit !== 0 && total >= totalLimit), + }; } diff --git a/src/git/parsers/remoteParser.ts b/src/git/parsers/remoteParser.ts index 7f0059d..4cfef2b 100644 --- a/src/git/parsers/remoteParser.ts +++ b/src/git/parsers/remoteParser.ts @@ -1,4 +1,4 @@ -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import type { GitRemoteType } from '../models/remote'; import { GitRemote } from '../models/remote'; import type { getRemoteProviderMatcher } from '../remotes/remoteProviders'; @@ -7,68 +7,67 @@ const emptyStr = ''; const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitRemoteParser { - @debug({ args: false, singleLine: true }) - static parse( - data: string, - repoPath: string, - remoteProviderMatcher: ReturnType, - ): GitRemote[] | undefined { - if (!data) return undefined; - - const remotes = new Map(); - - let name; - let url; - let type; - - let scheme; - let domain; - let path; - - let remote: GitRemote | undefined; - - let match; - do { - match = remoteRegex.exec(data); - if (match == null) break; - - [, name, url, type] = match; - - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - name = ` ${name}`.substr(1); - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - url = ` ${url}`.substr(1); - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - type = ` ${type}`.substr(1); - - [scheme, domain, path] = parseGitRemoteUrl(url); - - remote = remotes.get(name); - if (remote == null) { - remote = new GitRemote(repoPath, name, scheme, domain, path, remoteProviderMatcher(url, domain, path), [ - { url: url, type: type as GitRemoteType }, - ]); - remotes.set(name, remote); - } else { - remote.urls.push({ url: url, type: type as GitRemoteType }); - if (remote.provider != null && type !== 'push') continue; - - if (remote.provider?.hasRichIntegration()) { - remote.provider.dispose(); - } - - const provider = remoteProviderMatcher(url, domain, path); - if (provider == null) continue; - - remote = new GitRemote(repoPath, name, scheme, domain, path, provider, remote.urls); - remotes.set(name, remote); +export function parseGitRemotes( + data: string, + repoPath: string, + remoteProviderMatcher: ReturnType, +): GitRemote[] { + using sw = maybeStopWatch(`Git.parseRemotes(${repoPath})`, { log: false, logLevel: 'debug' }); + if (!data) return []; + + const remotes = new Map(); + + let name; + let url; + let type; + + let scheme; + let domain; + let path; + + let remote: GitRemote | undefined; + + let match; + do { + match = remoteRegex.exec(data); + if (match == null) break; + + [, name, url, type] = match; + + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + name = ` ${name}`.substr(1); + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + url = ` ${url}`.substr(1); + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + type = ` ${type}`.substr(1); + + [scheme, domain, path] = parseGitRemoteUrl(url); + + remote = remotes.get(name); + if (remote == null) { + remote = new GitRemote(repoPath, name, scheme, domain, path, remoteProviderMatcher(url, domain, path), [ + { url: url, type: type as GitRemoteType }, + ]); + remotes.set(name, remote); + } else { + remote.urls.push({ url: url, type: type as GitRemoteType }); + if (remote.provider != null && type !== 'push') continue; + + if (remote.provider?.hasRichIntegration()) { + remote.provider.dispose(); } - } while (true); - return [...remotes.values()]; - } + const provider = remoteProviderMatcher(url, domain, path); + if (provider == null) continue; + + remote = new GitRemote(repoPath, name, scheme, domain, path, provider, remote.urls); + remotes.set(name, remote); + } + } while (true); + + sw?.stop({ suffix: ` parsed ${remotes.size} remotes` }); + + return [...remotes.values()]; } // Test git urls diff --git a/src/git/parsers/statusParser.ts b/src/git/parsers/statusParser.ts index e295b9f..10eb8d0 100644 --- a/src/git/parsers/statusParser.ts +++ b/src/git/parsers/statusParser.ts @@ -1,5 +1,5 @@ -import { debug } from '../../system/decorators/log'; import { normalizePath } from '../../system/path'; +import { maybeStopWatch } from '../../system/stopwatch'; import { GitStatus, GitStatusFile } from '../models/status'; const emptyStr = ''; @@ -7,137 +7,137 @@ const emptyStr = ''; const aheadStatusV1Regex = /(?:ahead ([0-9]+))/; const behindStatusV1Regex = /(?:behind ([0-9]+))/; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitStatusParser { - @debug({ args: false, singleLine: true }) - static parse(data: string, repoPath: string, porcelainVersion: number): GitStatus | undefined { - if (!data) return undefined; +export function parseGitStatus(data: string, repoPath: string, porcelainVersion: number): GitStatus | undefined { + using sw = maybeStopWatch(`Git.parseStatus(${repoPath}, v=${porcelainVersion})`, { + log: false, + logLevel: 'debug', + }); + if (!data) return undefined; - const lines = data.split('\n').filter((i?: T): i is T => Boolean(i)); - if (lines.length === 0) return undefined; + const lines = data.split('\n').filter((i?: T): i is T => Boolean(i)); + if (lines.length === 0) return undefined; - if (porcelainVersion < 2) return this.parseV1(lines, repoPath); + const status = porcelainVersion < 2 ? parseStatusV1(lines, repoPath) : parseStatusV2(lines, repoPath); - return this.parseV2(lines, repoPath); - } + sw?.stop({ suffix: ` parsed ${status.files.length} files` }); - @debug({ args: false, singleLine: true }) - private static parseV1(lines: string[], repoPath: string): GitStatus { - let branch: string | undefined; - const files = []; - const state = { - ahead: 0, - behind: 0, - }; - let upstream; - - let position = -1; - while (++position < lines.length) { - const line = lines[position]; - // Header - if (line.startsWith('##')) { - const lineParts = line.split(' '); - [branch, upstream] = lineParts[1].split('...'); - if (lineParts.length > 2) { - const upstreamStatus = lineParts.slice(2).join(' '); - - const aheadStatus = aheadStatusV1Regex.exec(upstreamStatus); - state.ahead = aheadStatus == null ? 0 : Number(aheadStatus[1]) || 0; - - const behindStatus = behindStatusV1Regex.exec(upstreamStatus); - state.behind = behindStatus == null ? 0 : Number(behindStatus[1]) || 0; - } + return status; +} + +function parseStatusV1(lines: string[], repoPath: string): GitStatus { + let branch: string | undefined; + const files = []; + const state = { + ahead: 0, + behind: 0, + }; + let upstream; + + let position = -1; + while (++position < lines.length) { + const line = lines[position]; + // Header + if (line.startsWith('##')) { + const lineParts = line.split(' '); + [branch, upstream] = lineParts[1].split('...'); + if (lineParts.length > 2) { + const upstreamStatus = lineParts.slice(2).join(' '); + + const aheadStatus = aheadStatusV1Regex.exec(upstreamStatus); + state.ahead = aheadStatus == null ? 0 : Number(aheadStatus[1]) || 0; + + const behindStatus = behindStatusV1Regex.exec(upstreamStatus); + state.behind = behindStatus == null ? 0 : Number(behindStatus[1]) || 0; + } + } else { + const rawStatus = line.substring(0, 2); + const fileName = line.substring(3); + if (rawStatus.startsWith('R') || rawStatus.startsWith('C')) { + const [file1, file2] = fileName.replace(/"/g, emptyStr).split('->'); + files.push(parseStatusFile(repoPath, rawStatus, file2.trim(), file1.trim())); } else { - const rawStatus = line.substring(0, 2); - const fileName = line.substring(3); - if (rawStatus.startsWith('R') || rawStatus.startsWith('C')) { - const [file1, file2] = fileName.replace(/"/g, emptyStr).split('->'); - files.push(this.parseStatusFile(repoPath, rawStatus, file2.trim(), file1.trim())); - } else { - files.push(this.parseStatusFile(repoPath, rawStatus, fileName)); - } + files.push(parseStatusFile(repoPath, rawStatus, fileName)); } } - - return new GitStatus(normalizePath(repoPath), branch ?? emptyStr, emptyStr, files, state, upstream); } - @debug({ args: false, singleLine: true }) - private static parseV2(lines: string[], repoPath: string): GitStatus { - let branch: string | undefined; - const files = []; - let sha: string | undefined; - const state = { - ahead: 0, - behind: 0, - }; - let upstream; - - let position = -1; - while (++position < lines.length) { - const line = lines[position]; - // Headers - if (line.startsWith('#')) { - const lineParts = line.split(' '); - switch (lineParts[1]) { - case 'branch.oid': - sha = lineParts[2]; - break; - case 'branch.head': - branch = lineParts[2]; - break; - case 'branch.upstream': - upstream = lineParts[2]; - break; - case 'branch.ab': - state.ahead = Number(lineParts[2].substring(1)); - state.behind = Number(lineParts[3].substring(1)); - break; - } - } else { - const lineParts = line.split(' '); - switch (lineParts[0][0]) { - case '1': // normal - files.push(this.parseStatusFile(repoPath, lineParts[1], lineParts.slice(8).join(' '))); - break; - case '2': { - // rename - const file = lineParts.slice(9).join(' ').split('\t'); - files.push(this.parseStatusFile(repoPath, lineParts[1], file[0], file[1])); - break; - } - case 'u': // unmerged - files.push(this.parseStatusFile(repoPath, lineParts[1], lineParts.slice(10).join(' '))); - break; - case '?': // untracked - files.push(this.parseStatusFile(repoPath, '??', lineParts.slice(1).join(' '))); - break; + return new GitStatus(normalizePath(repoPath), branch ?? emptyStr, emptyStr, files, state, upstream); +} + +function parseStatusV2(lines: string[], repoPath: string): GitStatus { + let branch: string | undefined; + const files = []; + let sha: string | undefined; + const state = { + ahead: 0, + behind: 0, + }; + let upstream; + + let position = -1; + while (++position < lines.length) { + const line = lines[position]; + // Headers + if (line.startsWith('#')) { + const lineParts = line.split(' '); + switch (lineParts[1]) { + case 'branch.oid': + sha = lineParts[2]; + break; + case 'branch.head': + branch = lineParts[2]; + break; + case 'branch.upstream': + upstream = lineParts[2]; + break; + case 'branch.ab': + state.ahead = Number(lineParts[2].substring(1)); + state.behind = Number(lineParts[3].substring(1)); + break; + } + } else { + const lineParts = line.split(' '); + switch (lineParts[0][0]) { + case '1': // normal + files.push(parseStatusFile(repoPath, lineParts[1], lineParts.slice(8).join(' '))); + break; + case '2': { + // rename + const file = lineParts.slice(9).join(' ').split('\t'); + files.push(parseStatusFile(repoPath, lineParts[1], file[0], file[1])); + break; } + case 'u': // unmerged + files.push(parseStatusFile(repoPath, lineParts[1], lineParts.slice(10).join(' '))); + break; + case '?': // untracked + files.push(parseStatusFile(repoPath, '??', lineParts.slice(1).join(' '))); + break; } } - - return new GitStatus(normalizePath(repoPath), branch ?? emptyStr, sha ?? emptyStr, files, state, upstream); } - static parseStatusFile( - repoPath: string, - rawStatus: string, - fileName: string, - originalFileName?: string, - ): GitStatusFile { - let x = !rawStatus.startsWith('.') ? rawStatus[0].trim() : undefined; - if (x == null || x.length === 0) { - x = undefined; - } + return new GitStatus(normalizePath(repoPath), branch ?? emptyStr, sha ?? emptyStr, files, state, upstream); +} - let y = undefined; - if (rawStatus.length > 1) { - y = rawStatus[1] !== '.' ? rawStatus[1].trim() : undefined; - if (y == null || y.length === 0) { - y = undefined; - } - } +function parseStatusFile( + repoPath: string, + rawStatus: string, + fileName: string, + originalFileName?: string, +): GitStatusFile { + let x = !rawStatus.startsWith('.') ? rawStatus[0].trim() : undefined; + if (x == null || x.length === 0) { + x = undefined; + } - return new GitStatusFile(repoPath, x, y, fileName, originalFileName); + let y = undefined; + if (rawStatus.length > 1) { + y = rawStatus[1] !== '.' ? rawStatus[1].trim() : undefined; + if (y == null || y.length === 0) { + y = undefined; + } } + + return new GitStatusFile(repoPath, x, y, fileName, originalFileName); } diff --git a/src/git/parsers/tagParser.ts b/src/git/parsers/tagParser.ts index 26a59fc..a63ce47 100644 --- a/src/git/parsers/tagParser.ts +++ b/src/git/parsers/tagParser.ts @@ -1,4 +1,4 @@ -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import { GitTag } from '../models/tag'; const tagRegex = /^(.+)<\*r>(.*)(.*)(.*)(.*)(.*)$/gm; @@ -7,54 +7,53 @@ const tagRegex = /^(.+)<\*r>(.*)(.*)(.*)(.*)(.*)$/gm; const lb = '%3c'; // `%${'<'.charCodeAt(0).toString(16)}`; const rb = '%3e'; // `%${'>'.charCodeAt(0).toString(16)}`; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitTagParser { - static defaultFormat = [ - `${lb}n${rb}%(refname)`, // tag name - `${lb}*r${rb}%(*objectname)`, // ref - `${lb}r${rb}%(objectname)`, // ref - `${lb}d${rb}%(creatordate:iso8601)`, // created date - `${lb}ad${rb}%(authordate:iso8601)`, // author date - `${lb}s${rb}%(subject)`, // message - ].join(''); - - @debug({ args: false, singleLine: true }) - static parse(data: string, repoPath: string): GitTag[] | undefined { - if (!data) return undefined; - - const tags: GitTag[] = []; - - let name; - let ref1; - let ref2; - let date; - let commitDate; - let message; - - let match; - do { - match = tagRegex.exec(data); - if (match == null) break; - - [, name, ref1, ref2, date, commitDate, message] = match; - - // Strip off refs/tags/ - name = name.substr(10); - - tags.push( - new GitTag( - repoPath, - name, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${ref1 || ref2}`.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - ` ${message}`.substr(1), - date ? new Date(date) : undefined, - commitDate == null || commitDate.length === 0 ? undefined : new Date(commitDate), - ), - ); - } while (true); - - return tags; - } +export const parseGitTagsDefaultFormat = [ + `${lb}n${rb}%(refname)`, // tag name + `${lb}*r${rb}%(*objectname)`, // ref + `${lb}r${rb}%(objectname)`, // ref + `${lb}d${rb}%(creatordate:iso8601)`, // created date + `${lb}ad${rb}%(authordate:iso8601)`, // author date + `${lb}s${rb}%(subject)`, // message +].join(''); + +export function parseGitTags(data: string, repoPath: string): GitTag[] { + using sw = maybeStopWatch(`Git.parseTags(${repoPath})`, { log: false, logLevel: 'debug' }); + + const tags: GitTag[] = []; + if (!data) return tags; + + let name; + let ref1; + let ref2; + let date; + let commitDate; + let message; + + let match; + do { + match = tagRegex.exec(data); + if (match == null) break; + + [, name, ref1, ref2, date, commitDate, message] = match; + + // Strip off refs/tags/ + name = name.substr(10); + + tags.push( + new GitTag( + repoPath, + name, + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${ref1 || ref2}`.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + ` ${message}`.substr(1), + date ? new Date(date) : undefined, + commitDate == null || commitDate.length === 0 ? undefined : new Date(commitDate), + ), + ); + } while (true); + + sw?.stop({ suffix: ` parsed ${tags.length} tags` }); + + return tags; } diff --git a/src/git/parsers/treeParser.ts b/src/git/parsers/treeParser.ts index 25b4bae..153680b 100644 --- a/src/git/parsers/treeParser.ts +++ b/src/git/parsers/treeParser.ts @@ -1,40 +1,39 @@ -import { debug } from '../../system/decorators/log'; +import { maybeStopWatch } from '../../system/stopwatch'; import type { GitTreeEntry } from '../models/tree'; const emptyStr = ''; const treeRegex = /(?:.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+)/gm; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitTreeParser { - @debug({ args: false, singleLine: true }) - static parse(data: string | undefined): GitTreeEntry[] | undefined { - if (!data) return undefined; - - const trees: GitTreeEntry[] = []; - - let type; - let sha; - let size; - let filePath; - - let match; - do { - match = treeRegex.exec(data); - if (match == null) break; - - [, type, sha, size, filePath] = match; - - trees.push({ - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - commitSha: sha == null || sha.length === 0 ? emptyStr : ` ${sha}`.substr(1), - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - path: filePath == null || filePath.length === 0 ? emptyStr : ` ${filePath}`.substr(1), - size: Number(size) || 0, - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - type: (type == null || type.length === 0 ? emptyStr : ` ${type}`.substr(1)) as 'blob' | 'tree', - }); - } while (true); - - return trees; - } +export function parseGitTree(data: string | undefined): GitTreeEntry[] { + using sw = maybeStopWatch(`Git.parseTree`, { log: false, logLevel: 'debug' }); + + const trees: GitTreeEntry[] = []; + if (!data) return trees; + + let type; + let sha; + let size; + let filePath; + + let match; + do { + match = treeRegex.exec(data); + if (match == null) break; + + [, type, sha, size, filePath] = match; + + trees.push({ + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + commitSha: sha == null || sha.length === 0 ? emptyStr : ` ${sha}`.substr(1), + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + path: filePath == null || filePath.length === 0 ? emptyStr : ` ${filePath}`.substr(1), + size: Number(size) || 0, + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + type: (type == null || type.length === 0 ? emptyStr : ` ${type}`.substr(1)) as 'blob' | 'tree', + }); + } while (true); + + sw?.stop({ suffix: ` parsed ${trees.length} trees` }); + + return trees; } diff --git a/src/git/parsers/worktreeParser.ts b/src/git/parsers/worktreeParser.ts index 230037b..816cb58 100644 --- a/src/git/parsers/worktreeParser.ts +++ b/src/git/parsers/worktreeParser.ts @@ -1,6 +1,6 @@ import { Uri } from 'vscode'; -import { debug } from '../../system/decorators/log'; import { normalizePath } from '../../system/path'; +import { maybeStopWatch } from '../../system/stopwatch'; import { getLines } from '../../system/string'; import { GitWorktree } from '../models/worktree'; @@ -14,88 +14,87 @@ interface WorktreeEntry { prunable?: boolean | string; } -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GitWorktreeParser { - @debug({ args: false, singleLine: true }) - static parse(data: string, repoPath: string): GitWorktree[] { - if (!data) return []; +export function parseGitWorktrees(data: string, repoPath: string): GitWorktree[] { + using sw = maybeStopWatch(`Git.parseWorktrees(${repoPath})`, { log: false, logLevel: 'debug' }); - if (repoPath != null) { - repoPath = normalizePath(repoPath); - } - - const worktrees: GitWorktree[] = []; + const worktrees: GitWorktree[] = []; + if (!data) return worktrees; - let entry: Partial | undefined = undefined; - let line: string; - let index: number; - let key: string; - let value: string; - let locked: string; - let prunable: string; - let main = true; // the first worktree is the main worktree + if (repoPath != null) { + repoPath = normalizePath(repoPath); + } - for (line of getLines(data)) { - index = line.indexOf(' '); - if (index === -1) { - key = line; - value = ''; - } else { - key = line.substring(0, index); - value = line.substring(index + 1); - } + let entry: Partial | undefined = undefined; + let line: string; + let index: number; + let key: string; + let value: string; + let locked: string; + let prunable: string; + let main = true; // the first worktree is the main worktree - if (key.length === 0 && entry != null) { - worktrees.push( - new GitWorktree( - main, - entry.bare ? 'bare' : entry.detached ? 'detached' : 'branch', - repoPath, - Uri.file(entry.path!), - entry.locked ?? false, - entry.prunable ?? false, - entry.sha, - entry.branch, - ), - ); + for (line of getLines(data)) { + index = line.indexOf(' '); + if (index === -1) { + key = line; + value = ''; + } else { + key = line.substring(0, index); + value = line.substring(index + 1); + } - entry = undefined; - main = false; - continue; - } + if (key.length === 0 && entry != null) { + worktrees.push( + new GitWorktree( + main, + entry.bare ? 'bare' : entry.detached ? 'detached' : 'branch', + repoPath, + Uri.file(entry.path!), + entry.locked ?? false, + entry.prunable ?? false, + entry.sha, + entry.branch, + ), + ); - if (entry == null) { - entry = {}; - } + entry = undefined; + main = false; + continue; + } - switch (key) { - case 'worktree': - entry.path = value; - break; - case 'bare': - entry.bare = true; - break; - case 'HEAD': - entry.sha = value; - break; - case 'branch': - // Strip off refs/heads/ - entry.branch = value.substr(11); - break; - case 'detached': - entry.detached = true; - break; - case 'locked': - [, locked] = value.split(' ', 2); - entry.locked = locked?.trim() || true; - break; - case 'prunable': - [, prunable] = value.split(' ', 2); - entry.prunable = prunable?.trim() || true; - break; - } + if (entry == null) { + entry = {}; } - return worktrees; + switch (key) { + case 'worktree': + entry.path = value; + break; + case 'bare': + entry.bare = true; + break; + case 'HEAD': + entry.sha = value; + break; + case 'branch': + // Strip off refs/heads/ + entry.branch = value.substr(11); + break; + case 'detached': + entry.detached = true; + break; + case 'locked': + [, locked] = value.split(' ', 2); + entry.locked = locked?.trim() || true; + break; + case 'prunable': + [, prunable] = value.split(' ', 2); + entry.prunable = prunable?.trim() || true; + break; + } } + + sw?.stop({ suffix: ` parsed ${worktrees.length} worktrees` }); + + return worktrees; } diff --git a/src/system/stopwatch.ts b/src/system/stopwatch.ts index 73c30a8..9e4351e 100644 --- a/src/system/stopwatch.ts +++ b/src/system/stopwatch.ts @@ -3,7 +3,10 @@ import type { LogProvider } from './logger'; import { defaultLogProvider } from './logger'; import type { LogLevel } from './logger.constants'; import type { LogScope } from './logger.scope'; -import { getNextLogScopeId } from './logger.scope'; +import { getNewLogScope } from './logger.scope'; + +(Symbol as any).dispose ??= Symbol('Symbol.dispose'); +(Symbol as any).asyncDispose ??= Symbol('Symbol.asyncDispose'); type StopwatchLogOptions = { message?: string; suffix?: string }; type StopwatchOptions = { @@ -13,8 +16,8 @@ type StopwatchOptions = { }; type StopwatchLogLevel = Exclude; -export class Stopwatch { - private readonly instance = `[${String(getNextLogScopeId()).padStart(5)}] `; +export class Stopwatch implements Disposable { + private readonly logScope: LogScope; private readonly logLevel: StopwatchLogLevel; private readonly logProvider: LogProvider; @@ -23,17 +26,10 @@ export class Stopwatch { return this._time; } - constructor( - private readonly scope: string | LogScope | undefined, - options?: StopwatchOptions, - ...params: any[] - ) { - let logScope; - if (typeof scope !== 'string') { - logScope = scope; - scope = ''; - this.instance = ''; - } + private _stopped = false; + + constructor(scope: string | LogScope | undefined, options?: StopwatchOptions, ...params: any[]) { + this.logScope = scope != null && typeof scope !== 'string' ? scope : getNewLogScope(scope ?? ''); let logOptions: StopwatchLogOptions | undefined; if (typeof options?.log === 'boolean') { @@ -52,57 +48,51 @@ export class Stopwatch { if (params.length) { this.logProvider.log( this.logLevel, - logScope, - `${this.instance}${scope}${logOptions.message ?? ''}${logOptions.suffix ?? ''}`, + this.logScope, + `${logOptions.message ?? ''}${logOptions.suffix ?? ''}`, ...params, ); } else { this.logProvider.log( this.logLevel, - logScope, - `${this.instance}${scope}${logOptions.message ?? ''}${logOptions.suffix ?? ''}`, + this.logScope, + `${logOptions.message ?? ''}${logOptions.suffix ?? ''}`, ); } } } + [Symbol.dispose](): void { + this.stop(); + } + elapsed(): number { const [secs, nanosecs] = hrtime(this._time); return secs * 1000 + Math.floor(nanosecs / 1000000); } log(options?: StopwatchLogOptions): void { - this.logCore(this.scope, options, false); + this.logCore(options, false); } restart(options?: StopwatchLogOptions): void { - this.logCore(this.scope, options, true); + this.logCore(options, true); this._time = hrtime(); + this._stopped = false; } stop(options?: StopwatchLogOptions): void { + if (this._stopped) return; + this.restart(options); + this._stopped = true; } - private logCore( - scope: string | LogScope | undefined, - options: StopwatchLogOptions | undefined, - logTotalElapsed: boolean, - ): void { + private logCore(options: StopwatchLogOptions | undefined, logTotalElapsed: boolean): void { if (!this.logProvider.enabled(this.logLevel)) return; - let logScope; - if (typeof scope !== 'string') { - logScope = scope; - scope = ''; - } - if (!logTotalElapsed) { - this.logProvider.log( - this.logLevel, - logScope, - `${this.instance}${scope}${options?.message ?? ''}${options?.suffix ?? ''}`, - ); + this.logProvider.log(this.logLevel, this.logScope, `${options?.message ?? ''}${options?.suffix ?? ''}`); return; } @@ -110,10 +100,10 @@ export class Stopwatch { const [secs, nanosecs] = hrtime(this._time); const ms = secs * 1000 + Math.floor(nanosecs / 1000000); - const prefix = `${this.instance}${scope}${options?.message ?? ''}`; + const prefix = options?.message ?? ''; this.logProvider.log( ms > 250 ? 'warn' : this.logLevel, - logScope, + this.logScope, `${prefix ? `${prefix} ` : ''}[${ms}ms]${options?.suffix ?? ''}`, ); } diff --git a/src/webviews/apps/tsconfig.json b/src/webviews/apps/tsconfig.json index a218684..a2c68ad 100644 --- a/src/webviews/apps/tsconfig.json +++ b/src/webviews/apps/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "jsx": "react", - "lib": ["dom", "dom.iterable", "es2022"], + "lib": ["dom", "dom.iterable", "es2022", "esnext.disposable"], "outDir": "../../", "paths": { "@env/*": ["src/env/browser/*"] diff --git a/tsconfig.base.json b/tsconfig.base.json index 4aeda77..4c5295e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -6,7 +6,7 @@ "forceConsistentCasingInFileNames": true, "incremental": true, "isolatedModules": true, - "lib": ["es2022"], + "lib": ["es2022", "esnext.disposable"], "module": "esnext", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, diff --git a/tsconfig.browser.json b/tsconfig.browser.json index 66a5af9..13a30b9 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "lib": ["dom", "dom.iterable", "es2022"], + "lib": ["dom", "dom.iterable", "es2022", "esnext.disposable"], "paths": { "@env/*": ["src/env/browser/*"], "path": ["node_modules/path-browserify"]