From 19e523d6e42cbaf18a273e8094ce1317d0a76535 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 22 May 2017 16:16:17 -0400 Subject: [PATCH] Adds diff info to the active line hover for uncommitted changes --- src/blameActiveLineController.ts | 42 +++++++++++++--- src/git/git.ts | 13 +++++ src/git/models/diff.ts | 18 +++++++ src/git/models/models.ts | 1 + src/git/parsers/diffParser.ts | 45 +++++++++++++++++ src/gitService.ts | 102 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 src/git/models/diff.ts create mode 100644 src/git/parsers/diffParser.ts diff --git a/src/blameActiveLineController.ts b/src/blameActiveLineController.ts index 0f40dae..769d26f 100644 --- a/src/blameActiveLineController.ts +++ b/src/blameActiveLineController.ts @@ -6,7 +6,7 @@ import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotati import { TextEditorComparer } from './comparers'; import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; -import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; +import { BlameabilityChangeEvent, GitCommit, GitContextTracker, GitService, GitUri, IGitCommitLine } from './gitService'; import * as moment from 'moment'; const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ @@ -133,11 +133,11 @@ export class BlameActiveLineController extends Disposable { this._blameable = editor !== undefined && editor.document !== undefined && !editor.document.isDirty; this._editor = editor; this._uri = await GitUri.fromUri(editor.document.uri, this.git); - + const maxLines = this._config.advanced.caching.statusBar.maxLines; - // If caching is on and the file is small enough -- kick off a blame for the whole file - if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { - this.git.getBlameForFile(this._uri); + // If caching is on and the file is small enough -- kick off a blame for the whole file + if (this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines)) { + this.git.getBlameForFile(this._uri); } this._updateBlame(editor.selection.active.line, editor); @@ -186,9 +186,9 @@ export class BlameActiveLineController extends Disposable { let commitLine: IGitCommitLine | undefined = undefined; // Since blame information isn't valid when there are unsaved changes -- don't show any status if (this._blameable && line >= 0) { - const blameLine = await this.git.getBlameForLine(this._uri, line); - commitLine = blameLine === undefined ? undefined : blameLine.line; - commit = blameLine === undefined ? undefined : blameLine.commit; + const blameLine = await this.git.getBlameForLine(this._uri, line); + commitLine = blameLine === undefined ? undefined : blameLine.line; + commit = blameLine === undefined ? undefined : blameLine.commit; } if (commit !== undefined && commitLine !== undefined) { @@ -297,7 +297,33 @@ export class BlameActiveLineController extends Disposable { // If we don't have a possible dupe or we aren't showing annotations get the hover message if (!commit.isUncommitted && (!possibleDuplicate || !this.annotationController.isAnnotating(editor))) { hoverMessage = BlameAnnotationFormatter.getAnnotationHover(cfg, blameLine, logCommit || commit); + + // if (commit.previousSha !== undefined) { + // const changes = await this.git.getDiffForLine(this._uri.repoPath, this._uri.fsPath, blameLine.line + offset, commit.previousSha); + // if (changes !== undefined) { + // const previous = changes[0]; + // if (previous !== undefined) { + // hoverMessage += `\n\n\`Before ${commit.shortSha}\`\n\`\`\`\n${previous.trim().replace(/\n/g, '\`\n>\n> \`')}\n\`\`\``; + // } + // else { + // hoverMessage += `\n\n\`Added in ${commit.shortSha}\``; + // } + // } + // } } + else if (commit.isUncommitted) { + const changes = await this.git.getDiffForLine(this._uri.repoPath, this._uri.fsPath, blameLine.line + offset); + if (changes !== undefined) { + let original = changes[0]; + if (original !== undefined) { + original = original.replace(/\n/g, '\`\n>\n> \`').trim(); + hoverMessage = `\`${'0'.repeat(8)}\`   __Uncommitted change__\n\n\---\n\`\`\`\n${original}\n\`\`\``; + } + // else { + // hoverMessage = `\`${'0'.repeat(8)}\`   __Uncommitted change__\n\n\`Added\``; + // } + } + } } let decorationOptions: DecorationOptions | undefined = undefined; diff --git a/src/git/git.ts b/src/git/git.ts index 24ec990..bd70779 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -9,6 +9,7 @@ import * as tmp from 'tmp'; export { IGit }; export * from './models/models'; export * from './parsers/blameParser'; +export * from './parsers/diffParser'; export * from './parsers/logParser'; export * from './parsers/stashParser'; export * from './parsers/statusParser'; @@ -160,6 +161,18 @@ export class Git { } } + static diff(repoPath: string, fileName: string, sha1?: string, sha2?: string) { + const params = [`diff`, `--diff-filter=M`, `-M`]; + if (sha1) { + params.push(sha1); + } + if (sha2) { + params.push(sha2); + } + + return gitCommand(repoPath, ...params, '--', fileName); + } + static diff_nameStatus(repoPath: string, sha1?: string, sha2?: string) { const params = [`diff`, `--name-status`, `-M`]; if (sha1) { diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts new file mode 100644 index 0000000..6bda031 --- /dev/null +++ b/src/git/models/diff.ts @@ -0,0 +1,18 @@ +'use strict'; + +export interface IGitDiffChunk { + chunk?: string; + + original: (string | undefined)[]; + originalStart: number; + originalEnd: number; + + changes: (string | undefined)[]; + changesStart: number; + changesEnd: number; +} + +export interface IGitDiff { + diff?: string; + chunks: IGitDiffChunk[]; +} \ No newline at end of file diff --git a/src/git/models/models.ts b/src/git/models/models.ts index 1cda1a7..99063a6 100644 --- a/src/git/models/models.ts +++ b/src/git/models/models.ts @@ -2,6 +2,7 @@ export * from './blame'; export * from './branch'; export * from './commit'; +export * from './diff'; export * from './log'; export * from './logCommit'; export * from './remote'; diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts new file mode 100644 index 0000000..44f6c3d --- /dev/null +++ b/src/git/parsers/diffParser.ts @@ -0,0 +1,45 @@ +'use strict'; +import { IGitDiff, IGitDiffChunk } from './../git'; + +const unifiedDiffRegex = /^@@ -([\d]+),([\d]+) [+]([\d]+),([\d]+) @@([\s\S]*?)(?=^@@)/gm; + +export class GitDiffParser { + + static parse(data: string, debug: boolean = false): IGitDiff | undefined { + if (!data) return undefined; + + const chunks: IGitDiffChunk[] = []; + + let match: RegExpExecArray | null = null; + do { + match = unifiedDiffRegex.exec(`${data}\n@@`); + if (match == null) break; + + const originalStart = +match[1]; + const changedStart = +match[3]; + + const chunk = match[5]; + const lines = chunk.split('\n').slice(1); + const original = lines.filter(l => l[0] !== '+').map(l => (l[0] === '-') ? l.substring(1) : undefined); + const changed = lines.filter(l => l[0] !== '-').map(l => (l[0] === '+') ? l.substring(1) : undefined); + + chunks.push({ + chunk: debug ? chunk : undefined, + original: original, + originalStart: originalStart, + originalEnd: originalStart + +match[2], + changes: changed, + changesStart: changedStart, + changesEnd: changedStart + +match[4] + }); + } while (match != null); + + if (!chunks.length) return undefined; + + const diff = { + diff: debug ? data : undefined, + chunks: chunks + } as IGitDiff; + return diff; + } +} \ No newline at end of file diff --git a/src/gitService.ts b/src/gitService.ts index dd0115c..3d58910 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 { CodeLensVisibility, IConfig } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; -import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStash, IGitStatus } from './git/git'; +import { Git, GitBlameParser, GitBranch, GitCommit, GitDiffParser, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGit, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitDiff, IGitLog, IGitStash, IGitStatus } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { GitCodeLensProvider } from './gitCodeLensProvider'; import { Logger } from './logger'; @@ -25,7 +25,7 @@ class UriCacheEntry { class GitCacheEntry { - private cache: Map = new Map(); + private cache: Map = new Map(); constructor(public key: string) { } @@ -33,11 +33,11 @@ class GitCacheEntry { return Iterables.every(this.cache.values(), _ => _.errorMessage !== undefined); } - get (key: string): T | undefined { + get (key: string): T | undefined { return this.cache.get(key) as T; } - set (key: string, value: T) { + set (key: string, value: T) { this.cache.set(key, value); } } @@ -49,6 +49,7 @@ interface ICachedItem { } interface ICachedBlame extends ICachedItem { } +interface ICachedDiff extends ICachedItem { } interface ICachedLog extends ICachedItem { } enum RemoveCacheReason { @@ -88,7 +89,7 @@ export class GitService extends Disposable { private _fsWatcher: FileSystemWatcher | undefined; private _gitignore: Promise; - static EmptyPromise: Promise = Promise.resolve(undefined); + static EmptyPromise: Promise = Promise.resolve(undefined); constructor(private context: ExtensionContext, public repoPath: string) { super(() => this.dispose()); @@ -565,6 +566,97 @@ export class GitService extends Disposable { return entry && entry.uri; } + async getDiffForFile(repoPath: string | undefined, fileName: string, sha1?: string, sha2?: string): Promise { + let key: string = 'diff'; + if (sha1 !== undefined) { + key += `:${sha1}`; + } + if (sha2 !== undefined) { + key += `:${sha2}`; + } + + let entry: GitCacheEntry | undefined; + if (this.UseCaching) { + const cacheKey = this.getCacheEntryKey(fileName); + entry = this._gitCache.get(cacheKey); + + if (entry !== undefined) { + const cachedDiff = entry.get(key); + if (cachedDiff !== undefined) { + Logger.log(`Cached(${key}): getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); + return cachedDiff.item; + } + } + + Logger.log(`Not Cached(${key}): getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); + + if (entry === undefined) { + entry = new GitCacheEntry(cacheKey); + this._gitCache.set(entry.key, entry); + } + } + else { + Logger.log(`getDiffForFile('${repoPath}', '${fileName}', ${sha1}, ${sha2})`); + } + + const promise = this._getDiffForFile(repoPath, fileName, sha1, sha2, entry, key); + + if (entry) { + Logger.log(`Add log cache for '${entry.key}:${key}'`); + + entry.set(key, { + //date: new Date(), + item: promise + } as ICachedDiff); + } + + return promise; + } + + private async _getDiffForFile(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, entry: GitCacheEntry | undefined, key: string): Promise { + const [file, root] = Git.splitPath(fileName, repoPath, false); + + try { + const data = await Git.diff(root, file, sha1, sha2); + return GitDiffParser.parse(data, this.config.debug); + } + catch (ex) { + // Trap and cache expected diff errors + if (entry) { + const msg = ex && ex.toString(); + Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`); + + entry.set(key, { + //date: new Date(), + item: GitService.EmptyPromise, + errorMessage: msg + } as ICachedDiff); + + return await GitService.EmptyPromise as IGitDiff; + } + + return undefined; + } + } + + async getDiffForLine(repoPath: string | undefined, fileName: string, line: number, sha1?: string, sha2?: string): Promise<[string | undefined, string | undefined] | undefined> { + try { + const diff = await this.getDiffForFile(repoPath, fileName, sha1, sha2); + if (diff === undefined) return undefined; + + const chunk = diff.chunks.find(_ => Math.min(_.originalStart, _.changesStart) <= line && Math.max(_.originalEnd, _.changesEnd) >= line); + if (chunk === undefined) return undefined; + + return [ + chunk.original[line - chunk.originalStart + 1], + chunk.changes[line - chunk.changesStart + 1] + ]; + } + catch (ex) { + return undefined; + } + } + async getLogCommit(repoPath: string | undefined, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; async getLogCommit(repoPath: string | undefined, fileName: string, sha: string | undefined, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise; async getLogCommit(repoPath: string | undefined, fileName: string, shaOrOptions?: string | undefined | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise {