From 79b960cdf3309a7dc8b9176451f6caa5312ea1ab Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 28 May 2019 00:20:06 -0400 Subject: [PATCH] Adds changes stats for file commits --- src/git/formatters/commitFormatter.ts | 19 ++++----- src/git/git.ts | 4 +- src/git/models/logCommit.ts | 39 ++++++++++++++---- src/git/parsers/logParser.ts | 75 ++++++++++++++++++++++++++++++----- src/views/nodes/commitFileNode.ts | 8 +++- 5 files changed, 115 insertions(+), 30 deletions(-) diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts index 5e4af8d..e095998 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -10,7 +10,7 @@ import { import { DateStyle, FileAnnotationType } from '../../configuration'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitCommit, GitCommitType, GitLogCommit, GitRemote, GitService, GitUri } from '../gitService'; +import { GitCommit, GitLogCommit, GitRemote, GitService, GitUri } from '../gitService'; import { Strings } from '../../system'; import { FormatOptions, Formatter } from './formatter'; import * as emojis from '../../emojis.json'; @@ -143,20 +143,17 @@ export class CommitFormatter extends Formatter { } get changes() { - if (!(this._item instanceof GitLogCommit) || this._item.type === GitCommitType.LogFile) { - return this._padOrTruncate(emptyStr, this._options.tokenOptions.changes); - } - - return this._padOrTruncate(this._item.getFormattedDiffStatus(), this._options.tokenOptions.changes); + return this._padOrTruncate( + this._item instanceof GitLogCommit ? this._item.getFormattedDiffStatus() : emptyStr, + this._options.tokenOptions.changes + ); } get changesShort() { - if (!(this._item instanceof GitLogCommit) || this._item.type === GitCommitType.LogFile) { - return this._padOrTruncate(emptyStr, this._options.tokenOptions.changesShort); - } - return this._padOrTruncate( - this._item.getFormattedDiffStatus({ compact: true, separator: emptyStr }), + this._item instanceof GitLogCommit + ? this._item.getFormattedDiffStatus({ compact: true, separator: emptyStr }) + : emptyStr, this._options.tokenOptions.changesShort ); } diff --git a/src/git/git.ts b/src/git/git.ts index 6d5965c..e20d13d 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -685,7 +685,7 @@ export class Git { ) { const [file, root] = Git.splitPath(fileName, repoPath); - const params = ['log', '--name-status', `--format=${format}`]; + const params = ['log', `--format=${format}`]; if (maxCount && !reverse) { params.push(`-n${maxCount}`); @@ -697,7 +697,7 @@ export class Git { } if (startLine == null) { - params.push('--name-status'); + params.push('--numstat', '--summary'); } else { // Don't include --name-status or -s because Git won't honor it diff --git a/src/git/models/logCommit.ts b/src/git/models/logCommit.ts index a9d80d2..a5ee6a1 100644 --- a/src/git/models/logCommit.ts +++ b/src/git/models/logCommit.ts @@ -6,6 +6,12 @@ import { GitUri } from '../gitUri'; import { GitCommit, GitCommitType } from './commit'; import { GitFile, GitFileStatus } from './file'; +const emptyStats = Object.freeze({ + added: 0, + deleted: 0, + changed: 0 +}); + export interface GitLogCommitLine { from: { line: number; @@ -36,6 +42,12 @@ export class GitLogCommit extends GitCommit { originalFileName?: string | undefined, previousSha?: string | undefined, previousFileName?: string | undefined, + private readonly _fileStats?: + | { + insertions: number; + deletions: number; + } + | undefined, public readonly parentShas?: string[], public readonly line?: GitLogCommitLine ) { @@ -69,13 +81,21 @@ export class GitLogCommit extends GitCommit { @memoize() getDiffStatus() { + if (this._fileStats !== undefined) { + return { + added: this._fileStats.insertions, + deleted: this._fileStats.deletions, + changed: 0 + }; + } + + if (this.isFile || this.files.length === 0) return emptyStats; + const diff = { added: 0, deleted: 0, changed: 0 }; - if (this.files.length === 0) return diff; - for (const f of this.files) { switch (f.status) { case 'A': @@ -113,21 +133,24 @@ export class GitLogCommit extends GitCommit { if (added === 0 && changed === 0 && deleted === 0) return empty || ''; if (expand) { + const type = this.isFile ? 'line' : 'file'; + let status = ''; if (added) { - status += `${Strings.pluralize('file', added)} added`; + status += `${Strings.pluralize(type, added)} added`; } if (changed) { - status += `${status.length === 0 ? '' : separator}${Strings.pluralize('file', changed)} changed`; + status += `${status.length === 0 ? '' : separator}${Strings.pluralize(type, changed)} changed`; } if (deleted) { - status += `${status.length === 0 ? '' : separator}${Strings.pluralize('file', deleted)} deleted`; + status += `${status.length === 0 ? '' : separator}${Strings.pluralize(type, deleted)} deleted`; } return `${prefix}${status}${suffix}`; } + // When `isFile` we are getting line changes -- and we can't get changed lines (only inserts and deletes) return `${prefix}${compact && added === 0 ? '' : `+${added}${separator}`}${ - compact && changed === 0 ? '' : `~${changed}${separator}` + (compact || this.isFile) && changed === 0 ? '' : `~${changed}${separator}` }${compact && deleted === 0 ? '' : `-${deleted}`}${suffix}`; } @@ -195,7 +218,9 @@ export class GitLogCommit extends GitCommit { this.getChangedValue(changes.originalFileName, this.originalFileName), this.getChangedValue(changes.previousSha, this.previousSha), this.getChangedValue(changes.previousFileName, this.previousFileName), - undefined + this._fileStats, + this.parentShas, + this.line ); } } diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index d011f13..861fcf4 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -12,6 +12,10 @@ const diffRegex = /diff --git a\/(.*) b\/(.*)/; const diffRangeRegex = /^@@ -(\d+?),(\d+?) \+(\d+?),(\d+?) @@/; export const fileStatusRegex = /(\S)\S*\t([^\t\n]+)(?:\t(.+))?/; +const fileStatusAndSummaryRegex = /^(\d+?|-)\s+?(\d+?|-)\s+?(.*)(?:\n\s(delete|rename|create))?/; +const fileStatusAndSummaryRenamedFileRegex = /(.+)\s=>\s(.+)/; +const fileStatusAndSummaryRenamedFilePathRegex = /(.*?){(.+?)\s=>\s(.+?)}(.*)/; + const logFileSimpleRegex = /^ (.*)\s*(?:(?:diff --git a\/(.*) b\/(.*))|(?:(\S)\S*\t([^\t\n]+)(?:\t(.+))?))/gm; const logFileSimpleRenamedRegex = /^ (\S+)\s*(.*)$/s; const logFileSimpleRenamedFilesRegex = /^(\S)\S*\t([^\t\n]+)(?:\t(.+)?)$/gm; @@ -37,6 +41,10 @@ interface LogEntry { files?: GitFile[]; status?: GitFileStatus; + fileStats?: { + insertions: number; + deletions: number; + }; summary?: string; @@ -99,6 +107,7 @@ export class GitLogParser { let match; let renamedFileName; + let renamedMatch; while (true) { next = lines.next(); @@ -242,18 +251,64 @@ export class GitLogParser { break; } else { - match = fileStatusRegex.exec(line); + next = lines.next(); + match = fileStatusAndSummaryRegex.exec(`${line}\n${next.value}`); if (match != null) { - entry.status = match[1] as GitFileStatus; - renamedFileName = match[3]; - if (renamedFileName !== undefined) { - entry.fileName = renamedFileName; - entry.originalFileName = match[2]; - } - else { - entry.fileName = match[2]; + entry.fileStats = { + insertions: Number(match[1]) || 0, + deletions: Number(match[2]) || 0 + }; + + switch (match[4]) { + case undefined: + entry.status = 'M' as GitFileStatus; + entry.fileName = match[3]; + break; + case 'rename': + entry.status = 'R' as GitFileStatus; + + renamedFileName = match[3]; + renamedMatch = fileStatusAndSummaryRenamedFilePathRegex.exec( + renamedFileName + ); + if (renamedMatch != null) { + entry.fileName = `${renamedMatch[1]}${renamedMatch[3]}${ + renamedMatch[4] + }`; + entry.originalFileName = `${renamedMatch[1]}${renamedMatch[2]}${ + renamedMatch[4] + }`; + } + else { + renamedMatch = fileStatusAndSummaryRenamedFileRegex.exec( + renamedFileName + ); + if (renamedMatch != null) { + entry.fileName = renamedMatch[2]; + entry.originalFileName = renamedMatch[1]; + } + else { + entry.fileName = renamedFileName; + } + } + + break; + case 'create': + entry.status = 'A' as GitFileStatus; + entry.fileName = match[3]; + break; + case 'delete': + entry.status = 'D' as GitFileStatus; + entry.fileName = match[3]; + break; + default: + entry.status = 'M' as GitFileStatus; + entry.fileName = match[3]; + break; } } + + if (next.done || next.value === '') break; } } } @@ -355,6 +410,7 @@ export class GitLogParser { const originalFileName = entry.originalFileName || (relativeFileName !== entry.fileName ? entry.fileName : undefined); + if (type === GitCommitType.LogFile) { entry.files = [ { @@ -380,6 +436,7 @@ export class GitLogParser { originalFileName, type === GitCommitType.Log ? entry.parentShas![0] : undefined, undefined, + entry.fileStats, entry.parentShas!, entry.line ); diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index 32b07da..4f50e7f 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -162,7 +162,13 @@ export class CommitFileNode extends ViewRefFileNode { this._tooltip = CommitFormatter.fromTemplate( this.commit.isUncommitted ? `\${author} ${GlyphChars.Dash} \${id}\n${status}\n\${ago} (\${date})` - : `\${author} ${GlyphChars.Dash} \${id}\n${status}\n\${ago} (\${date})\n\n\${message}`, + : `\${author} ${ + GlyphChars.Dash + } \${id}\n${status}\n\${ago} (\${date})\n\n\${message}${this.commit.getFormattedDiffStatus({ + expand: true, + prefix: '\n\n', + separator: '\n' + })}`, this.commit, { dateFormat: Container.config.defaultDateFormat