diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0f9a8..cdd85aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ --- ## Release Notes +### 1.3.0 + + - Adds support for blame and history (log) on files opened via compare commands -- allows for deep navigation through git history + ### 1.2.0 - Adds compare (working vs previous) options to repository history diff --git a/README.md b/README.md index aa24863..5a1dc64 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,6 @@ Provides Git CodeLens information (most recent commit, # of authors), on-demand ## Known Issues -- Content in the **history explorers** disappears after a bit: [vscode issue](https://github.com/Microsoft/vscode/issues/11360) +- Content in the **history explorers** disappears after a bit: [vscode issue](https://github.com/Microsoft/vscode/issues/16098) - CodeLens aren't updated properly after a file is saved: [vscode issue](https://github.com/Microsoft/vscode/issues/11546) - Visible whitespace causes issue with blame overlay (currently fixed with a hack, but infrequently and randomly fails): [vscode issue](https://github.com/Microsoft/vscode/issues/11485) diff --git a/package.json b/package.json index 76949ea..2387e6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlens", - "version": "1.2.0", + "version": "1.3.0", "author": { "name": "Eric Amodio", "email": "eamodio@gmail.com" diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 2a5d396..a2c8643 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -44,9 +44,10 @@ export default class BlameAnnotationController extends Disposable { } async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { - if (!editor || !editor.document || editor.viewColumn === undefined) return false; + if (!editor || !editor.document) return false; + if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; - const currentProvider = this._annotationProviders.get(editor.viewColumn); + const currentProvider = this._annotationProviders.get(editor.viewColumn || -1); if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) { await currentProvider.setSelection(shaOrLine); return true; @@ -56,7 +57,7 @@ export default class BlameAnnotationController extends Disposable { if (!await provider.supportsBlame()) return false; if (currentProvider) { - await this.clear(currentProvider.editor.viewColumn, false); + await this.clear(currentProvider.editor.viewColumn || -1, false); } if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) { @@ -73,36 +74,38 @@ export default class BlameAnnotationController extends Disposable { this._visibleColumns = this._getVisibleColumns(window.visibleTextEditors); } - this._annotationProviders.set(editor.viewColumn, provider); + this._annotationProviders.set(editor.viewColumn || -1, provider); return provider.provideBlameAnnotation(shaOrLine); } async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { - if (!editor || !editor.document || editor.viewColumn === undefined) return false; + if (!editor || !editor.document) return false; + if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; - let provider = this._annotationProviders.get(editor.viewColumn); + let provider = this._annotationProviders.get(editor.viewColumn || -1); if (!provider) return this.showBlameAnnotation(editor, shaOrLine); - await this.clear(provider.editor.viewColumn); + await this.clear(provider.editor.viewColumn || -1); return false; } private _getVisibleColumns(editors: TextEditor[]): Set { const set: Set = new Set(); for (const e of editors) { - if (e.viewColumn === undefined) continue; - + if (e.viewColumn === undefined && !this.git.hasGitUriForFile(e)) continue; set.add(e.viewColumn); } return set; } private _onActiveTextEditorChanged(e: TextEditor) { - if (e.viewColumn === undefined || this._pendingWhitespaceToggles.size === 0) return; + if (this._pendingWhitespaceToggles.size === 0 || (e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) return; + + const viewColumn = e.viewColumn || -1; - if (this._pendingWhitespaceToggles.has(e.viewColumn)) { - Logger.log('ActiveTextEditorChanged:', `Remove pending whitespace toggle for column ${e.viewColumn}`); - this._pendingWhitespaceToggles.delete(e.viewColumn); + if (this._pendingWhitespaceToggles.has(viewColumn)) { + Logger.log('ActiveTextEditorChanged:', `Remove pending whitespace toggle for column ${viewColumn}`); + this._pendingWhitespaceToggles.delete(viewColumn); // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- toggle whitespace back on Logger.log('ActiveTextEditorChanged:', `Toggle whitespace rendering on`); @@ -137,8 +140,10 @@ export default class BlameAnnotationController extends Disposable { private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { this._visibleColumns = this._getVisibleColumns(window.visibleTextEditors); - Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${e.viewColumn}`); - await this.clear(e.viewColumn); + const viewColumn = e.viewColumn || -1; + + Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`); + await this.clear(viewColumn); for (const [key, p] of this._annotationProviders) { if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue; @@ -168,7 +173,7 @@ export default class BlameAnnotationController extends Disposable { Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`); const editor = window.activeTextEditor; - if (p.requiresRenderWhitespaceToggle && (editor && editor.viewColumn !== key)) { + if (p.requiresRenderWhitespaceToggle && (editor && (editor.viewColumn || -1) !== key)) { this.clear(key, false); if (!this._pendingWhitespaceToggleDisposable) { diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index 1bf3a43..fdaac87 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -47,7 +47,8 @@ export class BlameAnnotationProvider extends Disposable { } this.document = this.editor.document; - this._uri = GitUri.fromUri(this.document.uri); + this._uri = GitUri.fromUri(this.document.uri, this.git); + this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath); this._config = workspace.getConfiguration('gitlens').get('blame'); diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts index 50926fc..d0d51a3 100644 --- a/src/blameStatusBarController.ts +++ b/src/blameStatusBarController.ts @@ -71,13 +71,13 @@ export default class BlameStatusBarController extends Disposable { } private async _onActiveTextEditorChanged(e: TextEditor): Promise { - if (!e || !e.document || e.document.isUntitled || e.viewColumn === undefined) { + if (!e || !e.document || e.document.isUntitled || (e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) { this.clear(); return; } this._document = e.document; - this._uri = GitUri.fromUri(this._document.uri); + this._uri = GitUri.fromUri(this._document.uri, this.git); const maxLines = this._config.advanced.caching.statusBar.maxLines; this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || this._document.lineCount <= maxLines); if (this._useCaching) { diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index 36d562c..259551a 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -21,7 +21,7 @@ export default class DiffLineWithPreviousCommand extends EditorCommand { line = line || editor.selection.active.line; if (!commit || GitProvider.isUncommitted(commit.sha)) { - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index 64ab9a7..19b206c 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -21,7 +21,7 @@ export default class DiffLineWithWorkingCommand extends EditorCommand { line = line || editor.selection.active.line; if (!commit || GitProvider.isUncommitted(commit.sha)) { - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index 5733f3a..11a60c6 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -29,7 +29,7 @@ export default class DiffWithPreviousCommand extends EditorCommand { } if (!commit || rangeOrLine instanceof Range) { - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath, rangeOrLine); diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index 20febd0..a99c5ba 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -23,7 +23,7 @@ export default class DiffWithWorkingCommand extends EditorCommand { line = line || editor.selection.active.line; if (!commit || GitProvider.isUncommitted(commit.sha)) { - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath); @@ -37,7 +37,7 @@ export default class DiffWithWorkingCommand extends EditorCommand { } } - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const compare = await this.git.getVersionedFile(commit.uri.fsPath, commit.repoPath, commit.sha); diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index 7a502a8..61cd2ee 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -20,7 +20,7 @@ export default class ShowBlameHistoryCommand extends EditorCommand { position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const locations = await this.git.getBlameLocations(gitUri.fsPath, range, gitUri.sha, gitUri.repoPath, sha, line); diff --git a/src/commands/showFileHistory.ts b/src/commands/showFileHistory.ts index 1102048..d478b8d 100644 --- a/src/commands/showFileHistory.ts +++ b/src/commands/showFileHistory.ts @@ -19,7 +19,7 @@ export default class ShowFileHistoryCommand extends EditorCommand { position = editor.document.validateRange(new Range(0, 0, 0, 1000000)).start; } - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const locations = await this.git.getLogLocations(gitUri.fsPath, gitUri.sha, gitUri.repoPath, sha, line); diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 300aae7..dd1a9ca 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -19,7 +19,7 @@ export default class ShowQuickFileHistoryCommand extends EditorCommand { uri = editor.document.uri; } - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); try { const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath); diff --git a/src/commands/showQuickRepoHistory.ts b/src/commands/showQuickRepoHistory.ts index 469df4a..93189c2 100644 --- a/src/commands/showQuickRepoHistory.ts +++ b/src/commands/showQuickRepoHistory.ts @@ -23,7 +23,7 @@ export default class ShowQuickRepoHistoryCommand extends Command { try { let repoPath: string; if (uri instanceof Uri) { - const gitUri = GitUri.fromUri(uri); + const gitUri = GitUri.fromUri(uri, this.git); repoPath = gitUri.repoPath; if (!repoPath) { diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 538ad4e..5a1663e 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -50,7 +50,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { if (languageLocations.location === CodeLensLocation.None) return lenses; - const gitUri = GitUri.fromUri(document.uri); + const gitUri = GitUri.fromUri(document.uri, this.git); const blamePromise = this.git.getBlameForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath); let blame: IGitBlame; diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 1e11670..9def052 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -14,7 +14,11 @@ import * as path from 'path'; export { Git }; export * from './git/git'; -class CacheEntry { +class UriCacheEntry { + constructor(public uri: GitUri) { } +} + +class GitCacheEntry { blame?: ICachedBlame; log?: ICachedLog; @@ -39,8 +43,9 @@ enum RemoveCacheReason { } export default class GitProvider extends Disposable { - private _cache: Map | undefined; + private _gitCache: Map | undefined; private _cacheDisposable: Disposable | undefined; + private _uriCache: Map | undefined; private _config: IConfig; private _disposable: Disposable; @@ -85,11 +90,16 @@ export default class GitProvider extends Disposable { this._disposable && this._disposable.dispose(); this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose(); - this._cache && this._cache.clear(); + this._uriCache && this._uriCache.clear(); + this._gitCache && this._gitCache.clear(); + } + + public get UseUriCaching() { + return !!this._uriCache; } - public get UseCaching() { - return !!this._cache; + public get UseGitCaching() { + return !!this._gitCache; } private _onConfigure() { @@ -112,7 +122,8 @@ export default class GitProvider extends Disposable { if (advancedChanged) { if (config.advanced.caching.enabled) { // TODO: Cache needs to be cleared on file changes -- createFileSystemWatcher or timeout? - this._cache = new Map(); + this._gitCache = new Map(); + this._uriCache = new Map(); const disposables: Disposable[] = []; @@ -128,8 +139,12 @@ export default class GitProvider extends Disposable { else { this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable = undefined; - this._cache && this._cache.clear(); - this._cache = undefined; + + this._uriCache && this._uriCache.clear(); + this._uriCache = undefined; + + this._gitCache && this._gitCache.clear(); + this._gitCache = undefined; } } @@ -141,19 +156,26 @@ export default class GitProvider extends Disposable { } private _removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) { - if (!this.UseCaching) return; + if (!this.UseGitCaching) return; if (document.uri.scheme !== DocumentSchemes.File) return; const fileName = Git.normalizePath(document.fileName); const cacheKey = this._getCacheEntryKey(fileName); + if (reason === RemoveCacheReason.DocumentClosed) { + // Don't remove this from cache because at least for now DocumentClosed can't really be trusted + // It seems to fire when an editor is no longer visible (but the tab still is) + // if (this._fileCache.delete(cacheKey)) { + // Logger.log(`Clear uri cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`); + // } + // Don't remove broken blame on close (since otherwise we'll have to run the broken blame again) - const entry = this._cache.get(cacheKey); + const entry = this._gitCache.get(cacheKey); if (entry && entry.hasErrors) return; } - if (this._cache.delete(cacheKey)) { + if (this._gitCache.delete(cacheKey)) { Logger.log(`Clear cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`); // if (reason === RemoveCacheReason.DocumentSaved) { @@ -163,6 +185,32 @@ export default class GitProvider extends Disposable { } } + hasGitUriForFile(editor: TextEditor): boolean; + hasGitUriForFile(fileName: string): boolean; + hasGitUriForFile(fileNameOrEditor: string | TextEditor): boolean { + if (!this.UseUriCaching) return false; + + let fileName: string; + if (typeof fileNameOrEditor === 'string') { + fileName = fileNameOrEditor; + } + else { + if (!fileNameOrEditor || !fileNameOrEditor.document || !fileNameOrEditor.document.uri) return false; + fileName = fileNameOrEditor.document.uri.fsPath; + } + + const cacheKey = this._getCacheEntryKey(fileName); + return this._uriCache.has(cacheKey); + } + + getGitUriForFile(fileName: string) { + if (!this.UseUriCaching) return undefined; + + const cacheKey = this._getCacheEntryKey(fileName); + const entry = this._uriCache.get(cacheKey); + return entry && entry.uri; + } + getRepoPath(cwd: string): Promise { return Git.repoPath(cwd); } @@ -176,17 +224,17 @@ export default class GitProvider extends Disposable { Logger.log(`getBlameForFile('${fileName}', ${sha}, ${repoPath})`); fileName = Git.normalizePath(fileName); - const useCaching = this.UseCaching && !sha; + const useCaching = this.UseGitCaching && !sha; let cacheKey: string | undefined; - let entry: CacheEntry | undefined; + let entry: GitCacheEntry | undefined; if (useCaching) { cacheKey = this._getCacheEntryKey(fileName); - entry = this._cache.get(cacheKey); + entry = this._gitCache.get(cacheKey); if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; if (entry === undefined) { - entry = new CacheEntry(); + entry = new GitCacheEntry(); } } @@ -210,7 +258,7 @@ export default class GitProvider extends Disposable { errorMessage: msg }; - this._cache.set(cacheKey, entry); + this._gitCache.set(cacheKey, entry); return >GitProvider.EmptyPromise; } return undefined; @@ -225,7 +273,7 @@ export default class GitProvider extends Disposable { item: promise }; - this._cache.set(cacheKey, entry); + this._gitCache.set(cacheKey, entry); } return promise; @@ -234,7 +282,7 @@ export default class GitProvider extends Disposable { async getBlameForLine(fileName: string, line: number, sha?: string, repoPath?: string): Promise { Logger.log(`getBlameForLine('${fileName}', ${line}, ${sha}, ${repoPath})`); - if (this.UseCaching && !sha) { + if (this.UseGitCaching && !sha) { const blame = await this.getBlameForFile(fileName); const blameLine = blame && blame.lines[line]; if (!blameLine) return undefined; @@ -375,17 +423,17 @@ export default class GitProvider extends Disposable { Logger.log(`getLogForFile('${fileName}', ${sha}, ${repoPath}, ${range && `[${range.start.line}, ${range.end.line}]`})`); fileName = Git.normalizePath(fileName); - const useCaching = this.UseCaching && !range; + const useCaching = this.UseGitCaching && !range; let cacheKey: string; - let entry: CacheEntry; + let entry: GitCacheEntry; if (useCaching) { cacheKey = this._getCacheEntryKey(fileName); - entry = this._cache.get(cacheKey); + entry = this._gitCache.get(cacheKey); if (entry !== undefined && entry.log !== undefined) return entry.log.item; if (entry === undefined) { - entry = new CacheEntry(); + entry = new GitCacheEntry(); } } @@ -411,7 +459,7 @@ export default class GitProvider extends Disposable { errorMessage: msg }; - this._cache.set(cacheKey, entry); + this._gitCache.set(cacheKey, entry); return >GitProvider.EmptyPromise; } return undefined; @@ -426,7 +474,7 @@ export default class GitProvider extends Disposable { item: promise }; - this._cache.set(cacheKey, entry); + this._gitCache.set(cacheKey, entry); } return promise; @@ -455,9 +503,16 @@ export default class GitProvider extends Disposable { return locations; } - getVersionedFile(fileName: string, repoPath: string, sha: string) { + async getVersionedFile(fileName: string, repoPath: string, sha: string) { Logger.log(`getVersionedFile('${fileName}', ${repoPath}, ${sha})`); - return Git.getVersionedFile(fileName, repoPath, sha); + + const file = await Git.getVersionedFile(fileName, repoPath, sha); + if (this.UseUriCaching) { + const cacheKey = this._getCacheEntryKey(file); + const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath, fileName })); + this._uriCache.set(cacheKey, entry); + } + return file; } getVersionedFileText(fileName: string, repoPath: string, sha: string) { @@ -524,12 +579,19 @@ export default class GitProvider extends Disposable { } } +export interface IGitCommitInfo { + sha: string; + repoPath: string; + fileName: string; + originalFileName?: string; +} + export class GitUri extends Uri { offset: number; repoPath?: string | undefined; sha?: string | undefined; - constructor(uri?: Uri, commit?: GitCommit) { + constructor(uri?: Uri, commit?: IGitCommitInfo) { super(); if (!uri) return; @@ -561,11 +623,12 @@ export class GitUri extends Uri { return Uri.file(this.fsPath); } - static fromCommit(uri: Uri, commit: GitCommit) { - return new GitUri(uri, commit); - } + static fromUri(uri: Uri, git?: GitProvider) { + if (git) { + const gitUri = git.getGitUriForFile(uri.fsPath); + if (gitUri) return gitUri; + } - static fromUri(uri: Uri) { return new GitUri(uri); } } diff --git a/src/gitRevisionCodeLensProvider.ts b/src/gitRevisionCodeLensProvider.ts index 87c83f0..845c1f6 100644 --- a/src/gitRevisionCodeLensProvider.ts +++ b/src/gitRevisionCodeLensProvider.ts @@ -22,7 +22,7 @@ export default class GitRevisionCodeLensProvider implements CodeLensProvider { constructor(context: ExtensionContext, private git: GitProvider) { } async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { - const gitUri = GitUri.fromUri(document.uri); + const gitUri = GitUri.fromUri(document.uri, this.git); const lenses: CodeLens[] = [];