From e6316400f0e42e663956fe3d465b3c67660e98f8 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 10 Jun 2017 00:16:46 -0400 Subject: [PATCH] Optimized parsers for speed & memory usage Switches to lazy parsing of diff chunks --- src/annotations/diffAnnotationProvider.ts | 2 +- src/commands/openCommitInRemote.ts | 4 +- src/git/models/blame.ts | 9 +- src/git/models/blameCommit.ts | 20 ++ src/git/models/commit.ts | 6 +- src/git/models/diff.ts | 37 ++- src/git/models/logCommit.ts | 5 +- src/git/models/models.ts | 1 + src/git/models/stashCommit.ts | 4 +- src/git/parsers/blameParser.ts | 181 ++++++--------- src/git/parsers/diffParser.ts | 73 +++--- src/git/parsers/logParser.ts | 265 +++++++++++----------- src/gitCodeLensProvider.ts | 16 +- src/gitService.ts | 45 ++-- src/system/iterable.ts | 2 +- src/system/string.ts | 13 ++ 16 files changed, 343 insertions(+), 340 deletions(-) create mode 100644 src/git/models/blameCommit.ts diff --git a/src/annotations/diffAnnotationProvider.ts b/src/annotations/diffAnnotationProvider.ts index 23db986..4baf345 100644 --- a/src/annotations/diffAnnotationProvider.ts +++ b/src/annotations/diffAnnotationProvider.ts @@ -41,7 +41,7 @@ export class DiffAnnotationProvider extends AnnotationProviderBase { const decorators: DecorationOptions[] = []; for (const chunk of diff.chunks) { - let count = chunk.currentStart - 2; + let count = chunk.currentPosition.start - 2; for (const change of chunk.current) { if (change === undefined) continue; diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index 90bc170..6832045 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -2,7 +2,7 @@ import { Arrays } from '../system'; import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { ActiveEditorCommand, Commands, getCommandUri } from './common'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitBlameCommit, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { OpenInRemoteCommandArgs } from './openInRemote'; @@ -33,7 +33,7 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { let commit = blame.commit; // If the line is uncommitted, find the previous commit if (commit.isUncommitted) { - commit = new GitCommit(commit.type, commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message); + commit = new GitBlameCommit(commit.repoPath, commit.previousSha!, commit.previousFileName!, commit.author, commit.date, commit.message, []); } const remotes = Arrays.uniqueBy(await this.git.getRemotes(gitUri.repoPath), _ => _.url, _ => !!_.provider); diff --git a/src/git/models/blame.ts b/src/git/models/blame.ts index 2634298..f2205f5 100644 --- a/src/git/models/blame.ts +++ b/src/git/models/blame.ts @@ -1,16 +1,17 @@ 'use strict'; -import { GitAuthor, GitCommit, GitCommitLine } from './commit'; +import { GitAuthor, GitCommitLine } from './commit'; +import { GitBlameCommit } from './blameCommit'; export interface GitBlame { repoPath: string; authors: Map; - commits: Map; + commits: Map; lines: GitCommitLine[]; } export interface GitBlameLine { author: GitAuthor; - commit: GitCommit; + commit: GitBlameCommit; line: GitCommitLine; } @@ -20,6 +21,6 @@ export interface GitBlameLines extends GitBlame { export interface GitBlameCommitLines { author: GitAuthor; - commit: GitCommit; + commit: GitBlameCommit; lines: GitCommitLine[]; } \ No newline at end of file diff --git a/src/git/models/blameCommit.ts b/src/git/models/blameCommit.ts new file mode 100644 index 0000000..640eae0 --- /dev/null +++ b/src/git/models/blameCommit.ts @@ -0,0 +1,20 @@ +'use strict'; +import { GitCommit, GitCommitLine } from './commit'; + +export class GitBlameCommit extends GitCommit { + + constructor( + repoPath: string, + sha: string, + fileName: string, + author: string, + date: Date, + message: string, + public lines: GitCommitLine[], + originalFileName?: string, + previousSha?: string, + previousFileName?: string + ) { + super('blame', repoPath, sha, fileName, author, date, message, originalFileName, previousSha, previousFileName); + } +} \ No newline at end of file diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 5649ac9..b3e7d46 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -21,7 +21,7 @@ export type GitCommitType = 'blame' | 'branch' | 'file' | 'stash'; export class GitCommit { type: GitCommitType; - lines: GitCommitLine[]; + // lines: GitCommitLine[]; originalFileName?: string; previousSha?: string; previousFileName?: string; @@ -36,7 +36,7 @@ export class GitCommit { public author: string, public date: Date, public message: string, - lines?: GitCommitLine[], + // lines?: GitCommitLine[], originalFileName?: string, previousSha?: string, previousFileName?: string @@ -44,7 +44,7 @@ export class GitCommit { this.type = type; this.fileName = this.fileName && this.fileName.replace(/, ?$/, ''); - this.lines = lines || []; + // this.lines = lines || []; this.originalFileName = originalFileName; this.previousSha = previousSha; this.previousFileName = previousFileName; diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts index 392b259..e774782 100644 --- a/src/git/models/diff.ts +++ b/src/git/models/diff.ts @@ -1,20 +1,41 @@ 'use strict'; +import { GitDiffParser } from '../parsers/diffParser'; export interface GitDiffLine { line: string; state: 'added' | 'removed' | 'unchanged'; } -export interface GitDiffChunk { - current: (GitDiffLine | undefined)[]; - currentStart: number; - currentEnd: number; +export class GitDiffChunk { - previous: (GitDiffLine | undefined)[]; - previousStart: number; - previousEnd: number; + private _chunk: string | undefined; + private _current: (GitDiffLine | undefined)[] | undefined; + private _previous: (GitDiffLine | undefined)[] | undefined; - chunk?: string; + constructor(chunk: string, public currentPosition: { start: number, end: number }, public previousPosition: { start: number, end: number }) { + this._chunk = chunk; + } + + get current(): (GitDiffLine | undefined)[] { + if (this._chunk !== undefined) { + this.parseChunk(); + } + + return this._current!; + } + + get previous(): (GitDiffLine | undefined)[] { + if (this._chunk !== undefined) { + this.parseChunk(); + } + + return this._previous!; + } + + private parseChunk() { + [this._current, this._previous] = GitDiffParser.parseChunk(this._chunk!); + this._chunk = undefined; + } } export interface GitDiff { diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index 3d1700f..86779e7 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -1,6 +1,6 @@ 'use strict'; import { Uri } from 'vscode'; -import { GitCommit, GitCommitLine, GitCommitType } from './commit'; +import { GitCommit, GitCommitType } from './commit'; import { GitStatusFileStatus, IGitStatusFile } from './status'; import * as path from 'path'; @@ -23,12 +23,11 @@ export class GitLogCommit extends GitCommit { message: string, status?: GitStatusFileStatus, fileStatuses?: IGitStatusFile[], - lines?: GitCommitLine[], originalFileName?: string, previousSha?: string, previousFileName?: string ) { - super(type, repoPath, sha, fileName, author, date, message, lines, originalFileName, previousSha, previousFileName); + super(type, repoPath, sha, fileName, author, date, message, originalFileName, previousSha, previousFileName); this.fileNames = this.fileName; diff --git a/src/git/models/models.ts b/src/git/models/models.ts index 99063a6..a1535ac 100644 --- a/src/git/models/models.ts +++ b/src/git/models/models.ts @@ -1,5 +1,6 @@ 'use strict'; export * from './blame'; +export * from './blameCommit'; export * from './branch'; export * from './commit'; export * from './diff'; diff --git a/src/git/models/stashCommit.ts b/src/git/models/stashCommit.ts index c19c835..301124c 100644 --- a/src/git/models/stashCommit.ts +++ b/src/git/models/stashCommit.ts @@ -1,5 +1,4 @@ 'use strict'; -import { GitCommitLine } from './commit'; import { GitLogCommit } from './logCommit'; import { GitStatusFileStatus, IGitStatusFile } from './status'; @@ -14,12 +13,11 @@ export class GitStashCommit extends GitLogCommit { message: string, status?: GitStatusFileStatus, fileStatuses?: IGitStatusFile[], - lines?: GitCommitLine[], originalFileName?: string, previousSha?: string, previousFileName?: string ) { - super('stash', repoPath, sha, fileName, 'You', date, message, status, fileStatuses, lines, originalFileName, previousSha, previousFileName); + super('stash', repoPath, sha, fileName, 'You', date, message, status, fileStatuses, originalFileName, previousSha, previousFileName); } get shortSha() { diff --git a/src/git/parsers/blameParser.ts b/src/git/parsers/blameParser.ts index 0196c6a..47e0508 100644 --- a/src/git/parsers/blameParser.ts +++ b/src/git/parsers/blameParser.ts @@ -1,5 +1,6 @@ 'use strict'; -import { Git, GitAuthor, GitBlame, GitCommit, GitCommitLine } from './../git'; +import { Strings } from '../../system'; +import { Git, GitAuthor, GitBlame, GitBlameCommit, GitCommitLine } from './../git'; import * as moment from 'moment'; import * as path from 'path'; @@ -11,15 +12,9 @@ interface BlameEntry { lineCount: number; author: string; - // authorEmail?: string; authorDate?: string; authorTimeZone?: string; - // committer?: string; - // committerEmail?: string; - // committerDate?: string; - // committerTimeZone?: string; - previousSha?: string; previousFileName?: string; @@ -30,18 +25,25 @@ interface BlameEntry { export class GitBlameParser { - private static _parseEntries(data: string): BlameEntry[] | undefined { + static parse(data: string, repoPath: string | undefined, fileName: string): GitBlame | undefined { if (!data) return undefined; - const lines = data.split('\n'); - if (!lines.length) return undefined; + const authors: Map = new Map(); + const commits: Map = new Map(); + const lines: GitCommitLine[] = []; - const entries: BlameEntry[] = []; + let relativeFileName = repoPath && fileName; let entry: BlameEntry | undefined = undefined; - let position = -1; - while (++position < lines.length) { - const lineParts = lines[position].split(' '); + let line: string; + let lineParts: string[]; + + let i = -1; + let first = true; + + for (line of Strings.lines(data)) { + i++; + lineParts = line.split(' '); if (lineParts.length < 2) continue; if (entry === undefined) { @@ -62,10 +64,6 @@ export class GitBlameParser { : lineParts.slice(1).join(' ').trim(); break; - // case 'author-mail': - // entry.authorEmail = lineParts[1].trim(); - // break; - case 'author-time': entry.authorDate = lineParts[1]; break; @@ -74,22 +72,6 @@ export class GitBlameParser { entry.authorTimeZone = lineParts[1]; break; - // case 'committer': - // entry.committer = lineParts.slice(1).join(' ').trim(); - // break; - - // case 'committer-mail': - // entry.committerEmail = lineParts[1].trim(); - // break; - - // case 'committer-time': - // entry.committerDate = lineParts[1]; - // break; - - // case 'committer-tz': - // entry.committerTimeZone = lineParts[1]; - // break; - case 'summary': entry.summary = lineParts.slice(1).join(' ').trim(); break; @@ -102,7 +84,15 @@ export class GitBlameParser { case 'filename': entry.fileName = lineParts.slice(1).join(' '); - entries.push(entry); + if (first && repoPath === undefined) { + // Try to get the repoPath from the most recent commit + repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, '')); + relativeFileName = Git.normalizePath(path.relative(repoPath, fileName)); + } + first = false; + + GitBlameParser._parseEntry(entry, repoPath, relativeFileName, commits, authors, lines); + entry = undefined; break; @@ -111,71 +101,6 @@ export class GitBlameParser { } } - return entries; - } - - static parse(data: string, repoPath: string | undefined, fileName: string): GitBlame | undefined { - const entries = this._parseEntries(data); - if (!entries) return undefined; - - const authors: Map = new Map(); - const commits: Map = new Map(); - const lines: GitCommitLine[] = []; - - let relativeFileName = repoPath && fileName; - - for (let i = 0, len = entries.length; i < len; i++) { - const entry = entries[i]; - - if (i === 0 && repoPath === undefined) { - // Try to get the repoPath from the most recent commit - repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, '')); - relativeFileName = Git.normalizePath(path.relative(repoPath, fileName)); - } - - let commit = commits.get(entry.sha); - if (commit === undefined) { - if (entry.author !== undefined) { - let author = authors.get(entry.author); - if (author === undefined) { - author = { - name: entry.author, - lineCount: 0 - }; - authors.set(entry.author, author); - } - } - - commit = new GitCommit('blame', repoPath!, entry.sha, relativeFileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!); - - if (relativeFileName !== entry.fileName) { - commit.originalFileName = entry.fileName; - } - - if (entry.previousSha) { - commit.previousSha = entry.previousSha; - commit.previousFileName = entry.previousFileName; - } - - commits.set(entry.sha, commit); - } - - for (let j = 0, len = entry.lineCount; j < len; j++) { - const line: GitCommitLine = { - sha: entry.sha, - line: entry.line + j, - originalLine: entry.originalLine + j - }; - - if (commit.previousSha) { - line.previousSha = commit.previousSha; - } - - commit.lines.push(line); - lines[line.line] = line; - } - } - commits.forEach(c => { if (c.author === undefined) return; @@ -185,23 +110,57 @@ export class GitBlameParser { author.lineCount += c.lines.length; }); - const sortedAuthors: Map = new Map(); - // const values = - Array.from(authors.values()) - .sort((a, b) => b.lineCount - a.lineCount) - .forEach(a => sortedAuthors.set(a.name, a)); - - // const sortedCommits: Map = new Map(); - // Array.from(commits.values()) - // .sort((a, b) => b.date.getTime() - a.date.getTime()) - // .forEach(c => sortedCommits.set(c.sha, c)); + const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); return { repoPath: repoPath, authors: sortedAuthors, - // commits: sortedCommits, commits: commits, lines: lines } as GitBlame; } + + private static _parseEntry(entry: BlameEntry, repoPath: string | undefined, fileName: string | undefined, commits: Map, authors: Map, lines: GitCommitLine[]) { + let commit = commits.get(entry.sha); + if (commit === undefined) { + if (entry.author !== undefined) { + let author = authors.get(entry.author); + if (author === undefined) { + author = { + name: entry.author, + lineCount: 0 + }; + authors.set(entry.author, author); + } + } + + commit = new GitBlameCommit(repoPath!, entry.sha, fileName!, entry.author, moment(`${entry.authorDate} ${entry.authorTimeZone}`, 'X +-HHmm').toDate(), entry.summary!, []); + + if (fileName !== entry.fileName) { + commit.originalFileName = entry.fileName; + } + + if (entry.previousSha) { + commit.previousSha = entry.previousSha; + commit.previousFileName = entry.previousFileName; + } + + commits.set(entry.sha, commit); + } + + for (let i = 0, len = entry.lineCount; i < len; i++) { + const line: GitCommitLine = { + sha: entry.sha, + line: entry.line + i, + originalLine: entry.originalLine + i + }; + + if (commit.previousSha) { + line.previousSha = commit.previousSha; + } + + commit.lines.push(line); + lines[line.line] = line; + } + } } \ No newline at end of file diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts index 444aa3e..855174f 100644 --- a/src/git/parsers/diffParser.ts +++ b/src/git/parsers/diffParser.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Iterables, Strings } from '../../system'; import { GitDiff, GitDiffChunk, GitDiffLine } from './../git'; const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; @@ -19,44 +20,7 @@ export class GitDiffParser { const currentStart = +match[3]; const chunk = match[5]; - const lines = chunk.split('\n').slice(1); - - const current: (GitDiffLine | undefined)[] = []; - const previous: (GitDiffLine | undefined)[] = []; - for (const l of lines) { - switch (l[0]) { - case '+': - current.push({ - line: ` ${l.substring(1)}`, - state: 'added' - }); - previous.push(undefined); - break; - - case '-': - current.push(undefined); - previous.push({ - line: ` ${l.substring(1)}`, - state: 'removed' - }); - break; - - default: - current.push({ line: l, state: 'unchanged' }); - previous.push({ line: l, state: 'unchanged' }); - break; - } - } - - chunks.push({ - chunk: debug ? chunk : undefined, - current: current, - currentStart: currentStart, - currentEnd: currentStart + +match[4], - previous: previous, - previousStart: previousStart, - previousEnd: previousStart + +match[2] - }); + chunks.push(new GitDiffChunk(chunk, { start: currentStart, end: currentStart + +match[4] }, { start: previousStart, end: previousStart + +match[2] })); } while (match != null); if (!chunks.length) return undefined; @@ -67,4 +31,37 @@ export class GitDiffParser { } as GitDiff; return diff; } + + static parseChunk(chunk: string): [(GitDiffLine | undefined)[], (GitDiffLine | undefined)[]] { + const lines = Iterables.skip(Strings.lines(chunk), 1); + + const current: (GitDiffLine | undefined)[] = []; + const previous: (GitDiffLine | undefined)[] = []; + for (const l of lines) { + switch (l[0]) { + case '+': + current.push({ + line: ` ${l.substring(1)}`, + state: 'added' + }); + previous.push(undefined); + break; + + case '-': + current.push(undefined); + previous.push({ + line: ` ${l.substring(1)}`, + state: 'removed' + }); + break; + + default: + current.push({ line: l, state: 'unchanged' }); + previous.push({ line: l, state: 'unchanged' }); + break; + } + } + + return [current, previous]; + } } \ No newline at end of file diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 695e66a..08db403 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Strings } from '../../system'; import { Range } from 'vscode'; import { Git, GitAuthor, GitCommitType, GitLog, GitLogCommit, GitStatusFileStatus, IGitStatusFile } from './../git'; // import { Logger } from '../../logger'; @@ -11,9 +12,6 @@ interface LogEntry { author: string; authorDate?: string; - // committer?: string; - // committerDate?: string; - parentShas?: string[]; fileName?: string; @@ -29,24 +27,47 @@ const diffRegex = /diff --git a\/(.*) b\/(.*)/; export class GitLogParser { - private static _parseEntries(data: string, type: GitCommitType, maxCount: number | undefined, reverse: boolean): LogEntry[] | undefined { + static parse(data: string, type: GitCommitType, repoPath: string | undefined, fileName: string | undefined, sha: string | undefined, maxCount: number | undefined, reverse: boolean, range: Range | undefined): GitLog | undefined { if (!data) return undefined; - const lines = data.split('\n'); - if (!lines.length) return undefined; + const authors: Map = new Map(); + const commits: Map = new Map(); - const entries: LogEntry[] = []; + let relativeFileName: string; + let recentCommit: GitLogCommit | undefined = undefined; + + if (repoPath !== undefined) { + repoPath = Git.normalizePath(repoPath); + } let entry: LogEntry | undefined = undefined; - let position = -1; - while (++position < lines.length) { - // Since log --reverse doesn't properly honor a max count -- enforce it here - if (reverse && maxCount && (entries.length >= maxCount)) break; + let line: string | undefined = undefined; + let lineParts: string[]; + let next: IteratorResult | undefined = undefined; - let lineParts = lines[position].split(' '); - if (lineParts.length < 2) { - continue; + let i = -1; + let first = true; + let skip = false; + + const lines = Strings.lines(data); + // for (line of lines) { + while (true) { + if (!skip) { + next = lines.next(); + if (next.done) break; + + line = next.value; + i++; } + else { + skip = false; + } + + // Since log --reverse doesn't properly honor a max count -- enforce it here + if (reverse && maxCount && (i >= maxCount)) break; + + lineParts = line!.split(' '); + if (lineParts.length < 2) continue; if (entry === undefined) { if (!Git.shaRegex.test(lineParts[0])) continue; @@ -69,47 +90,54 @@ export class GitLogParser { entry.authorDate = `${lineParts[1]}T${lineParts[2]}${lineParts[3]}`; break; - // case 'committer': - // entry.committer = lineParts.slice(1).join(' ').trim(); - // break; - - // case 'committer-date': - // entry.committerDate = lineParts.slice(1).join(' ').trim(); - // break; - case 'parents': entry.parentShas = lineParts.slice(1); break; case 'summary': entry.summary = lineParts.slice(1).join(' ').trim(); - while (++position < lines.length) { - const next = lines[position]; - if (!next) break; - if (next === 'filename ?') { - position--; + while (true) { + next = lines.next(); + if (next.done) break; + + i++; + line = next.value; + if (!line) break; + + if (line === 'filename ?') { + skip = true; break; } - entry.summary += `\n${lines[position]}`; + entry.summary += `\n${line}`; } break; case 'filename': if (type === 'branch') { - const nextLine = lines[position + 1]; - // If the next line isn't blank, make sure it isn't starting a new commit - if (nextLine && Git.shaRegex.test(nextLine)) continue; + next = lines.next(); + if (next.done) break; - position++; + i++; + line = next.value; + + // If the next line isn't blank, make sure it isn't starting a new commit + if (line && Git.shaRegex.test(line)) { + skip = true; + continue; + } let diff = false; - while (++position < lines.length) { - const line = lines[position]; + while (true) { + next = lines.next(); + if (next.done) break; + + i++; + line = next.value; lineParts = line.split(' '); if (Git.shaRegex.test(lineParts[0])) { - position--; + skip = true; break; } @@ -147,127 +175,90 @@ export class GitLogParser { } } else { - position += 2; - const line = lines[position]; + next = lines.next(); + next = lines.next(); + + i += 2; + line = next.value; + entry.status = line[0] as GitStatusFileStatus; entry.fileName = line.substring(1); this._parseFileName(entry); } - entries.push(entry); + if (first && repoPath === undefined && type === 'file' && fileName !== undefined) { + // Try to get the repoPath from the most recent commit + repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, '')); + relativeFileName = Git.normalizePath(path.relative(repoPath, fileName)); + } + else { + relativeFileName = entry.fileName!; + } + first = false; + + recentCommit = GitLogParser._parseEntry(entry, type, repoPath, relativeFileName, commits, authors, recentCommit); + entry = undefined; break; - - default: - break; } + + if (next!.done) break; + } - return entries; - } - - static parse(data: string, type: GitCommitType, repoPath: string | undefined, fileName: string | undefined, sha: string | undefined, maxCount: number | undefined, reverse: boolean, range: Range | undefined): GitLog | undefined { - const entries = this._parseEntries(data, type, maxCount, reverse); - if (!entries) return undefined; - - const authors: Map = new Map(); - const commits: Map = new Map(); - - let relativeFileName: string; - let recentCommit: GitLogCommit | undefined = undefined; - - if (repoPath !== undefined) { - repoPath = Git.normalizePath(repoPath); - } - - for (let i = 0, len = entries.length; i < len; i++) { - // Since log --reverse doesn't properly honor a max count -- enforce it here - if (reverse && maxCount && (i >= maxCount)) break; - - const entry = entries[i]; - - if (i === 0 && repoPath === undefined && type === 'file' && fileName !== undefined) { - // Try to get the repoPath from the most recent commit - repoPath = Git.normalizePath(fileName.replace(fileName.startsWith('/') ? `/${entry.fileName}` : entry.fileName!, '')); - relativeFileName = Git.normalizePath(path.relative(repoPath, fileName)); - } - else { - relativeFileName = entry.fileName!; - } - - let commit = commits.get(entry.sha); - if (commit === undefined) { - if (entry.author !== undefined) { - let author = authors.get(entry.author); - if (author === undefined) { - author = { - name: entry.author, - lineCount: 0 - }; - authors.set(entry.author, author); - } - } - - commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName); - commit.parentShas = entry.parentShas!; - - if (relativeFileName !== entry.fileName) { - commit.originalFileName = entry.fileName; - } - - commits.set(entry.sha, commit); - } - // else { - // Logger.log(`merge commit? ${entry.sha}`); - // } - - if (recentCommit !== undefined) { - recentCommit.previousSha = commit.sha; - - // If the commit sha's match (merge commit), just forward it along - commit.nextSha = commit.sha !== recentCommit.sha ? recentCommit.sha : recentCommit.nextSha; - - // Only add a filename if this is a file log - if (type === 'file') { - recentCommit.previousFileName = commit.originalFileName || commit.fileName; - commit.nextFileName = recentCommit.originalFileName || recentCommit.fileName; - } - } - recentCommit = commit; - } - - commits.forEach(c => { - if (c.author === undefined) return; - - const author = authors.get(c.author); - if (author === undefined) return; - - author.lineCount += c.lines.length; - }); - - const sortedAuthors: Map = new Map(); - // const values = - Array.from(authors.values()) - .sort((a, b) => b.lineCount - a.lineCount) - .forEach(a => sortedAuthors.set(a.name, a)); - - // const sortedCommits: Map = new Map(); - // Array.from(commits.values()) - // .sort((a, b) => b.date.getTime() - a.date.getTime()) - // .forEach(c => sortedCommits.set(c.sha, c)); - return { repoPath: repoPath, - authors: sortedAuthors, - // commits: sortedCommits, + authors: authors, commits: commits, sha: sha, maxCount: maxCount, range: range, - truncated: !!(maxCount && entries.length >= maxCount) + truncated: !!(maxCount && i >= maxCount) } as GitLog; } + private static _parseEntry(entry: LogEntry, type: GitCommitType, repoPath: string | undefined, relativeFileName: string, commits: Map, authors: Map, recentCommit: GitLogCommit | undefined): GitLogCommit | undefined { + let commit = commits.get(entry.sha); + if (commit === undefined) { + if (entry.author !== undefined) { + let author = authors.get(entry.author); + if (author === undefined) { + author = { + name: entry.author, + lineCount: 0 + }; + authors.set(entry.author, author); + } + } + + commit = new GitLogCommit(type, repoPath!, entry.sha, relativeFileName, entry.author, moment(entry.authorDate).toDate(), entry.summary!, entry.status, entry.fileStatuses, undefined, entry.originalFileName); + commit.parentShas = entry.parentShas!; + + if (relativeFileName !== entry.fileName) { + commit.originalFileName = entry.fileName; + } + + commits.set(entry.sha, commit); + } + // else { + // Logger.log(`merge commit? ${entry.sha}`); + // } + + if (recentCommit !== undefined) { + recentCommit.previousSha = commit.sha; + + // If the commit sha's match (merge commit), just forward it along + commit.nextSha = commit.sha !== recentCommit.sha ? recentCommit.sha : recentCommit.nextSha; + + // Only add a filename if this is a file log + if (type === 'file') { + recentCommit.previousFileName = commit.originalFileName || commit.fileName; + commit.nextFileName = recentCommit.originalFileName || recentCommit.fileName; + } + } + return commit; + } + private static _parseFileName(entry: { fileName?: string, originalFileName?: string }) { if (entry.fileName === undefined) return; diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index adbe083..a6ac62a 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -4,7 +4,7 @@ import { CancellationToken, CodeLens, CodeLensProvider, Command, commands, Docum import { Commands, DiffWithPreviousCommandArgs, ShowBlameHistoryCommandArgs, ShowFileHistoryCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from './commands'; import { BuiltInCommands, DocumentSchemes, ExtensionKey } from './constants'; import { CodeLensCommand, CodeLensLocations, ICodeLensLanguageLocation, IConfig } from './configuration'; -import { GitBlame, GitBlameLines, GitCommit, GitService, GitUri } from './gitService'; +import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from './gitService'; import { Logger } from './logger'; import * as moment from 'moment'; @@ -304,7 +304,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowBlameHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowBlameHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { let line = lens.range.start.line; if (commit) { const blameLine = commit.lines.find(_ => _.line === line); @@ -330,7 +330,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { let line = lens.range.start.line; if (commit) { const blameLine = commit.lines.find(_ => _.line === line); @@ -355,7 +355,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyDiffWithPreviousCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyDiffWithPreviousCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { if (commit === undefined) { const blameLine = blame.allLines[lens.range.start.line]; commit = blame.commits.get(blameLine.sha); @@ -375,7 +375,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickCommitDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowQuickCommitDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails, @@ -389,7 +389,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickCommitFileDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowQuickCommitFileDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails, @@ -403,7 +403,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowQuickFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: CodeLensCommand.ShowQuickFileHistory, @@ -417,7 +417,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickBranchHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitCommit): T { + _applyShowQuickBranchHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: CodeLensCommand.ShowQuickCurrentBranchHistory, diff --git a/src/gitService.ts b/src/gitService.ts index 3ebd507..71ad3db 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -4,7 +4,7 @@ import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, l import { CommandContext, setCommandContext } from './commands'; import { IConfig } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; -import { Git, GitAuthor, GitBlame, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitCommit, GitDiff, GitDiffLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git'; +import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitCommit, GitDiff, GitDiffLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { GitCodeLensProvider } from './gitCodeLensProvider'; import { Logger } from './logger'; @@ -395,7 +395,8 @@ export class GitService extends Disposable { try { const data = await Git.blame(root, file, uri.sha); - return GitBlameParser.parse(data, root, file); + const blame = GitBlameParser.parse(data, root, file); + return blame; } catch (ex) { // Trap and cache expected blame errors @@ -480,15 +481,14 @@ export class GitService extends Disposable { } const lines = blame.lines.slice(range.start.line, range.end.line + 1); - const shas: Set = new Set(); - lines.forEach(l => shas.add(l.sha)); + const shas = new Set(lines.map(l => l.sha)); const authors: Map = new Map(); - const commits: Map = new Map(); - blame.commits.forEach(c => { + const commits: Map = new Map(); + for (const c of blame.commits.values()) { if (!shas.has(c.sha)) return; - const commit: GitCommit = new GitCommit('blame', c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, + const commit = new GitBlameCommit(c.repoPath, c.sha, c.fileName, c.author, c.date, c.message, c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName); commits.set(c.sha, commit); @@ -502,12 +502,9 @@ export class GitService extends Disposable { } author.lineCount += commit.lines.length; - }); + } - const sortedAuthors: Map = new Map(); - Array.from(authors.values()) - .sort((a, b) => b.lineCount - a.lineCount) - .forEach(a => sortedAuthors.set(a.name, a)); + const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); return { authors: sortedAuthors, @@ -629,7 +626,8 @@ export class GitService extends Disposable { try { const data = await Git.diff(root, file, sha1, sha2); - return GitDiffParser.parse(data); + const diff = GitDiffParser.parse(data); + return diff; } catch (ex) { // Trap and cache expected diff errors @@ -654,12 +652,12 @@ export class GitService extends Disposable { const diff = await this.getDiffForFile(uri, sha1, sha2); if (diff === undefined) return [undefined, undefined]; - const chunk = diff.chunks.find(_ => _.currentStart <= line && _.currentEnd >= line); + const chunk = diff.chunks.find(_ => _.currentPosition.start <= line && _.currentPosition.end >= line); if (chunk === undefined) return [undefined, undefined]; // Search for the line (skipping deleted lines -- since they don't currently exist in the editor) // Keep track of the deleted lines for the original version - line = line - chunk.currentStart + 1; + line = line - chunk.currentPosition.start + 1; let count = 0; let deleted = 0; for (const l of chunk.current) { @@ -676,7 +674,7 @@ export class GitService extends Disposable { return [ chunk.previous[line + deleted - 1], - chunk.current[line + deleted + (chunk.currentStart - chunk.previousStart)] + chunk.current[line + deleted + (chunk.currentPosition.start - chunk.previousPosition.start)] ]; } catch (ex) { @@ -715,7 +713,8 @@ export class GitService extends Disposable { try { const data = await Git.log(repoPath, sha, maxCount, reverse); - return GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined); + const log = GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined); + return log; } catch (ex) { return undefined; @@ -748,7 +747,8 @@ export class GitService extends Disposable { try { const data = await Git.log_search(repoPath, searchArgs, maxCount); - return GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); + const log = GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); + return log; } catch (ex) { return undefined; @@ -830,7 +830,8 @@ export class GitService extends Disposable { try { const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1); - return GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range); + const log = GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range); + return log; } catch (ex) { // Trap and cache expected log errors @@ -915,7 +916,8 @@ export class GitService extends Disposable { Logger.log(`getStash('${repoPath}')`); const data = await Git.stash_list(repoPath); - return GitStashParser.parse(data, repoPath); + const stash = GitStashParser.parse(data, repoPath); + return stash; } async getStatusForFile(repoPath: string, fileName: string): Promise { @@ -936,7 +938,8 @@ export class GitService extends Disposable { const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; const data = await Git.status(repoPath, porcelainVersion); - return GitStatusParser.parse(data, repoPath, porcelainVersion); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + return status; } async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string) { diff --git a/src/system/iterable.ts b/src/system/iterable.ts index 2c818d3..b162c6a 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -92,7 +92,7 @@ export namespace Iterables { return source.next().value; } - export function* skip(source: Iterable | IterableIterator, count: number): Iterable { + export function* skip(source: Iterable | IterableIterator, count: number): Iterable | IterableIterator { let i = 0; for (const item of source) { if (i >= count) yield item; diff --git a/src/system/string.ts b/src/system/string.ts index 7e24f6d..607b76c 100644 --- a/src/system/string.ts +++ b/src/system/string.ts @@ -43,6 +43,19 @@ export namespace Strings { return new Function(`return \`${template}\`;`).call(context); } + export function* lines(s: string): IterableIterator { + let i = 0; + while (i < s.length) { + let j = s.indexOf('\n', i); + if (j === -1) { + j = s.length; + } + + yield s.substring(i, j); + i = j + 1; + } + } + export function padLeft(s: string, padTo: number, padding: string = '\u00a0') { const diff = padTo - s.length; return (diff <= 0) ? s : '\u00a0'.repeat(diff) + s;