diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index b91d9bf..5733f3a 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -32,7 +32,7 @@ export default class DiffWithPreviousCommand extends EditorCommand { const gitUri = GitUri.fromUri(uri); try { - const log = await this.git.getLogForFile(gitUri.fsPath, rangeOrLine); + const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath, rangeOrLine); if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); const sha = (commit && commit.sha) || gitUri.sha; diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index fb934a8..20febd0 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -26,7 +26,7 @@ export default class DiffWithWorkingCommand extends EditorCommand { const gitUri = GitUri.fromUri(uri); try { - const log = await this.git.getLogForFile(gitUri.fsPath); + const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath); if (!log) return window.showWarningMessage(`Unable to open diff. File is probably not under source control`); commit = (gitUri.sha && log.commits.get(gitUri.sha)) || Iterables.first(log.commits.values()); diff --git a/src/commands/showBlameHistory.ts b/src/commands/showBlameHistory.ts index f0d129f..7a502a8 100644 --- a/src/commands/showBlameHistory.ts +++ b/src/commands/showBlameHistory.ts @@ -10,7 +10,7 @@ export default class ShowBlameHistoryCommand extends EditorCommand { super(Commands.ShowBlameHistory); } - async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position) { + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, range?: Range, position?: Position, sha?: string, line?: number) { if (!(uri instanceof Uri)) { if (!editor.document) return undefined; uri = editor.document.uri; @@ -23,7 +23,7 @@ export default class ShowBlameHistoryCommand extends EditorCommand { const gitUri = GitUri.fromUri(uri); try { - const locations = await this.git.getBlameLocations(gitUri.fsPath, range); + const locations = await this.git.getBlameLocations(gitUri.fsPath, range, gitUri.sha, gitUri.repoPath, sha, line); if (!locations) return window.showWarningMessage(`Unable to show blame history. File is probably not under source control`); return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); diff --git a/src/commands/showFileHistory.ts b/src/commands/showFileHistory.ts index 72e7b2e..cf367a8 100644 --- a/src/commands/showFileHistory.ts +++ b/src/commands/showFileHistory.ts @@ -22,7 +22,7 @@ export default class ShowFileHistoryCommand extends EditorCommand { const gitUri = GitUri.fromUri(uri); try { - const locations = await this.git.getLogLocations(gitUri.fsPath, sha, line); + const locations = await this.git.getLogLocations(gitUri.fsPath, gitUri.sha, gitUri.repoPath, sha, line); if (!locations) return window.showWarningMessage(`Unable to show history. File is probably not under source control`); return commands.executeCommand(BuiltInCommands.ShowReferences, uri, position, locations); diff --git a/src/constants.ts b/src/constants.ts index ac8a837..4ecdff5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,8 +30,7 @@ export const Commands = { export type DocumentSchemes = 'file' | 'git' | 'git-blame'; export const DocumentSchemes = { File: 'file' as DocumentSchemes, - Git: 'git' as DocumentSchemes, - GitBlame: 'git-blame' as DocumentSchemes + Git: 'git' as DocumentSchemes }; export type WorkspaceState = 'hasGitHistoryExtension' | 'repoPath'; diff --git a/src/extension.ts b/src/extension.ts index 898333c..28ed7a0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,22 +2,21 @@ import { ExtensionContext, extensions, languages, window, workspace } from 'vscode'; import BlameAnnotationController from './blameAnnotationController'; import BlameStatusBarController from './blameStatusBarController'; -import GitContentProvider from './gitContentProvider'; -import GitBlameCodeLensProvider from './gitBlameCodeLensProvider'; -import GitBlameContentProvider from './gitBlameContentProvider'; -import GitProvider, { Git } from './gitProvider'; -import { WorkspaceState } from './constants'; -import { IAdvancedConfig } from './configuration'; -import { Logger } from './logger'; -import DiffWithPreviousCommand from './commands/diffWithPrevious'; import DiffLineWithPreviousCommand from './commands/diffLineWithPrevious'; -import DiffWithWorkingCommand from './commands/diffWithWorking'; import DiffLineWithWorkingCommand from './commands/diffLineWithWorking'; +import DiffWithPreviousCommand from './commands/diffWithPrevious'; +import DiffWithWorkingCommand from './commands/diffWithWorking'; import ShowBlameCommand from './commands/showBlame'; import ShowBlameHistoryCommand from './commands/showBlameHistory'; import ShowFileHistoryCommand from './commands/showFileHistory'; import ToggleBlameCommand from './commands/toggleBlame'; import ToggleCodeLensCommand from './commands/toggleCodeLens'; +import { IAdvancedConfig } from './configuration'; +import { WorkspaceState } from './constants'; +import GitContentProvider from './gitContentProvider'; +import GitProvider, { Git } from './gitProvider'; +import GitRevisionCodeLensProvider from './gitRevisionCodeLensProvider'; +import { Logger } from './logger'; // this method is called when your extension is activated export async function activate(context: ExtensionContext) { @@ -50,9 +49,8 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(git); context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); - context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git))); - context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); + context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git))); const annotationController = new BlameAnnotationController(context, git); context.subscriptions.push(annotationController); diff --git a/src/git/git.ts b/src/git/git.ts index 01c23e0..ef0e471 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -66,7 +66,7 @@ export default class Git { const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); if (sha) { - return gitCommand(root, 'blame', format, '--root', `${sha}^`, '--', file); + return gitCommand(root, 'blame', format, '--root', `${sha}^!`, '--', file); } return gitCommand(root, 'blame', format, '--root', '--', file); } @@ -75,20 +75,26 @@ export default class Git { const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); if (sha) { - return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', `${sha}^`, '--', file); + return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', `${sha}^!`, '--', file); } return gitCommand(root, 'blame', `-L ${startLine},${endLine}`, format, '--root', '--', file); } - static log(fileName: string, repoPath?: string) { + static log(fileName: string, sha?: string, repoPath?: string) { const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); + if (sha) { + return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--date=iso8601-strict`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, `origin..${sha}`, '--', file); + } return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--date=iso8601-strict`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, file); } - static logRange(fileName: string, start: number, end: number, repoPath?: string) { + static logRange(fileName: string, start: number, end: number, sha?: string, repoPath?: string) { const [file, root]: [string, string] = Git.splitPath(Git.normalizePath(fileName), repoPath); + if (sha) { + return gitCommand(root, 'log', `--follow`, `--name-only`, `--no-merges`, `--date=iso8601-strict`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, `origin..${sha}`, `-L ${start},${end}:${file}`); + } return gitCommand(root, 'log', `--name-only`, `--no-merges`, `--date=iso8601-strict`, `--format=%H -%nauthor %an%nauthor-date %ai%ncommitter %cn%ncommitter-date %ci%nsummary %s%nfilename ?`, `-L ${start},${end}:${file}`); } diff --git a/src/gitBlameCodeLensProvider.ts b/src/gitBlameCodeLensProvider.ts deleted file mode 100644 index 5c59868..0000000 --- a/src/gitBlameCodeLensProvider.ts +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; -import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode'; -import { Commands, DocumentSchemes } from './constants'; -import GitProvider, { GitCommit } from './gitProvider'; -import * as path from 'path'; - -export class GitDiffWithWorkingTreeCodeLens extends CodeLens { - constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { - super(range); - } -} - -export class GitDiffWithPreviousCodeLens extends CodeLens { - constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { - super(range); - } -} - -export default class GitBlameCodeLensProvider implements CodeLensProvider { - static selector: DocumentSelector = { scheme: DocumentSchemes.GitBlame }; - - constructor(context: ExtensionContext, private git: GitProvider) { } - - provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { - const data = GitProvider.fromBlameUri(document.uri); - const fileName = data.fileName; - const sha = data.sha; - - return this.git.getBlameForFile(fileName).then(blame => { - const lenses: CodeLens[] = []; - if (!blame) return lenses; - - const commit = blame.commits.get(sha); - const absoluteFileName = path.join(commit.repoPath, fileName); - - // Add codelens to each "group" of blame lines - const lines = blame.lines.filter(l => l.sha === sha && l.originalLine >= data.range.start.line && l.originalLine <= data.range.end.line); - let lastLine = lines[0].originalLine; - lines.forEach(l => { - if (l.originalLine !== lastLine + 1) { - lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, absoluteFileName, commit, new Range(l.originalLine, 0, l.originalLine, 1))); - if (commit.previousSha) { - lenses.push(new GitDiffWithPreviousCodeLens(this.git, absoluteFileName, commit, new Range(l.originalLine, 1, l.originalLine, 2))); - } - } - lastLine = l.originalLine; - }); - - // Check if we have a lens for the whole document -- if not add one - if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { - lenses.push(new GitDiffWithWorkingTreeCodeLens(this.git, absoluteFileName, commit, new Range(0, 0, 0, 1))); - if (commit.previousSha) { - lenses.push(new GitDiffWithPreviousCodeLens(this.git, absoluteFileName, commit, new Range(0, 1, 0, 2))); - } - } - - return lenses; - }); - } - - resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { - if (lens instanceof GitDiffWithWorkingTreeCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token); - if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token); - return Promise.reject(undefined); - } - - _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingTreeCodeLens, token: CancellationToken): Thenable { - lens.command = { - title: `Compare with Working Tree`, - command: Commands.DiffWithWorking, - arguments: [ - Uri.file(lens.fileName), - lens.commit.sha, - lens.commit.uri, - lens.range.start.line - ] - }; - return Promise.resolve(lens); - } - - _resolveGitDiffWithPreviousCodeLens(lens: GitDiffWithPreviousCodeLens, token: CancellationToken): Thenable { - lens.command = { - title: `Compare with Previous (${lens.commit.previousSha})`, - command: Commands.DiffWithPrevious, - arguments: [ - Uri.file(lens.fileName), - lens.commit, - lens.range.start.line - ] - }; - return Promise.resolve(lens); - } -} diff --git a/src/gitBlameContentProvider.ts b/src/gitBlameContentProvider.ts deleted file mode 100644 index bc2031b..0000000 --- a/src/gitBlameContentProvider.ts +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; -import { EventEmitter, ExtensionContext, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, TextDocumentContentProvider, Uri, window } from 'vscode'; -import { DocumentSchemes } from './constants'; -import GitProvider, { IGitBlameUriData } from './gitProvider'; -import { Logger } from './logger'; -import * as moment from 'moment'; - -export default class GitBlameContentProvider implements TextDocumentContentProvider { - static scheme = DocumentSchemes.GitBlame; - - private _blameDecoration: TextEditorDecorationType; - private _onDidChange = new EventEmitter(); - //private _subscriptions: Disposable; - - constructor(context: ExtensionContext, private git: GitProvider) { - this._blameDecoration = window.createTextEditorDecorationType({ - dark: { - backgroundColor: 'rgba(255, 255, 255, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-dark.png'), - overviewRulerColor: 'rgba(255, 255, 255, 0.75)' - }, - light: { - backgroundColor: 'rgba(0, 0, 0, 0.15)', - gutterIconPath: context.asAbsolutePath('images/blame-light.png'), - overviewRulerColor: 'rgba(0, 0, 0, 0.75)' - }, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Right, - isWholeLine: true - }); - - //this._subscriptions = Disposable.from( - // window.onDidChangeActiveTextEditor(e => e ? Logger.log(e.document.uri) : Logger.log('active missing')), - //); - } - - dispose() { - this._onDidChange.dispose(); - //this._subscriptions && this._subscriptions.dispose(); - } - - get onDidChange() { - return this._onDidChange.event; - } - - public update(uri: Uri) { - this._onDidChange.fire(uri); - } - - provideTextDocumentContent(uri: Uri): string | Thenable { - const data = GitProvider.fromBlameUri(uri); - - //const editor = this._findEditor(Uri.file(join(data.repoPath, data.file))); - - return this.git.getVersionedFileText(data.originalFileName || data.fileName, data.repoPath, data.sha) - .then(text => { - this.update(uri); - - // TODO: This only works on the first load -- not after since it is cached - this._tryAddBlameDecorations(uri, data); - - // TODO: This needs to move to selection somehow to show on the main file editor - //this._addBlameDecorations(editor, data); - - return text; - }) - .catch(ex => Logger.error('[GitLens.GitBlameContentProvider]', 'getVersionedFileText', ex)); - - // return this.git.getVersionedFile(data.fileName, data.sha).then(dst => { - // let uri = Uri.parse(`file:${dst}`) - // return workspace.openTextDocument(uri).then(doc => { - // this.update(uri); - // return doc.getText(); - // }); - // }); - } - - private _findEditor(uri: Uri): TextEditor { - let uriString = uri.toString(); - const matcher = (e: TextEditor) => (e && e.document.uri.toString()) === uriString; - if (matcher(window.activeTextEditor)) { - return window.activeTextEditor; - } - return window.visibleTextEditors.find(matcher); - } - - private _tryAddBlameDecorations(uri: Uri, data: IGitBlameUriData) { - // Needs to be on a timer for some reason because we won't find the editor otherwise -- is there an event? - let handle = setInterval(() => { - let editor = this._findEditor(uri); - if (!editor) return; - - clearInterval(handle); - this.git.getBlameForShaRange(data.fileName, data.sha, data.range).then(blame => { - if (!blame || !blame.lines.length) return; - - editor.setDecorations(this._blameDecoration, blame.lines.map(l => { - return { - range: editor.document.validateRange(new Range(l.originalLine, 0, l.originalLine, 1000000)), - hoverMessage: `${blame.commit.message}\n${blame.commit.author}\n${moment(blame.commit.date).format('MMMM Do, YYYY hh:MMa')}\n${l.sha}` - }; - })); - }); - }, 200); - } - - // private _addBlameDecorations(editor: TextEditor, data: IGitBlameUriData) { - // editor.setDecorations(this._blameDecoration, data.lines.map(l => editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)))); - // } -} \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 1a9a1b4..1e214ec 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -185,7 +185,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { switch (this._config.codeLens.recentChange.command) { case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand(title, lens, blame); - case CodeLensCommand.ShowBlameHistory: return this._applyShowBlameHistoryCommand(title, lens, blame); + case CodeLensCommand.ShowBlameHistory: return this._applyShowBlameHistoryCommand(title, lens, blame, recentCommit); case CodeLensCommand.ShowFileHistory: return this._applyShowFileHistoryCommand(title, lens, blame, recentCommit); case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame, recentCommit); case CodeLensCommand.GitViewHistory: return this._applyGitHistoryCommand(title, lens, blame); @@ -218,27 +218,38 @@ export default class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowBlameHistoryCommand(title: string, lens: T, blame: IGitBlameLines) { + _applyShowBlameHistoryCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit) { + let line = lens.range.start.line; + if (commit) { + const blameLine = commit.lines.find(_ => _.line === line); + if (blameLine) { + line = blameLine.originalLine; + } + } + + const position = lens.isFullRange ? new Position(1, 0) : lens.range.start; lens.command = { title: title, command: Commands.ShowBlameHistory, - arguments: [Uri.file(lens.fileName), lens.blameRange, lens.range.start] + arguments: [Uri.file(lens.fileName), lens.blameRange, position, commit && commit.sha, line] }; return lens; } _applyShowFileHistoryCommand(title: string, lens: T, blame: IGitBlameLines, commit?: GitCommit) { let line = lens.range.start.line; - const blameLine = commit.lines.find(_ => _.line === line); - if (blameLine) { - line = blameLine.originalLine; + if (commit) { + const blameLine = commit.lines.find(_ => _.line === line); + if (blameLine) { + line = blameLine.originalLine; + } } const position = lens.isFullRange ? new Position(1, 0) : lens.range.start; lens.command = { title: title, command: Commands.ShowFileHistory, - arguments: [Uri.file(lens.fileName), position, commit.sha, line] + arguments: [Uri.file(lens.fileName), position, commit && commit.sha, line] }; return lens; } diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 4301f3f..32e4458 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -1,10 +1,10 @@ 'use strict'; import { Functions, Iterables, Objects } from './system'; import { Disposable, DocumentFilter, ExtensionContext, languages, Location, Position, Range, TextDocument, TextEditor, Uri, window, workspace } from 'vscode'; -import { DocumentSchemes, WorkspaceState } from './constants'; import { CodeLensVisibility, IConfig } from './configuration'; +import { DocumentSchemes, WorkspaceState } from './constants'; +import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog } from './git/git'; import GitCodeLensProvider from './gitCodeLensProvider'; -import Git, { GitBlameParserEnricher, GitBlameFormat, GitCommit, GitLogParserEnricher, IGitAuthor, IGitBlame, IGitBlameCommitLines, IGitBlameLine, IGitBlameLines, IGitLog } from './git/git'; import { Logger } from './logger'; import * as fs from 'fs'; import * as ignore from 'ignore'; @@ -264,10 +264,10 @@ export default class GitProvider extends Disposable { } } - async getBlameForRange(fileName: string, range: Range): Promise { - Logger.log(`getBlameForRange('${fileName}', ${range})`); + async getBlameForRange(fileName: string, range: Range, sha?: string, repoPath?: string): Promise { + Logger.log(`getBlameForRange('${fileName}', ${range}, ${sha}, ${repoPath})`); - const blame = await this.getBlameForFile(fileName); + const blame = await this.getBlameForFile(fileName, sha, repoPath); if (!blame) return undefined; if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame); @@ -314,27 +314,10 @@ export default class GitProvider extends Disposable { }; } - async getBlameForShaRange(fileName: string, sha: string, range: Range): Promise { - Logger.log(`getBlameForShaRange('${fileName}', ${sha}, ${range})`); + async getBlameLocations(fileName: string, range: Range, sha?: string, repoPath?: string, selectedSha?: string, line?: number): Promise { + Logger.log(`getBlameLocations('${fileName}', ${range}, ${sha}, ${repoPath})`); - const blame = await this.getBlameForFile(fileName); - if (!blame) return undefined; - - const lines = blame.lines.slice(range.start.line, range.end.line + 1).filter(l => l.sha === sha); - let commit = blame.commits.get(sha); - commit = new GitCommit(commit.repoPath, commit.sha, commit.fileName, commit.author, commit.date, commit.message, - lines, commit.originalFileName, commit.previousSha, commit.previousFileName); - return { - author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }), - commit: commit, - lines: lines - }; - } - - async getBlameLocations(fileName: string, range: Range): Promise { - Logger.log(`getBlameForShaRange('${fileName}', ${range})`); - - const blame = await this.getBlameForRange(fileName, range); + const blame = await this.getBlameForRange(fileName, range, sha, repoPath); if (!blame) return undefined; const commitCount = blame.commits.size; @@ -343,18 +326,21 @@ export default class GitProvider extends Disposable { Iterables.forEach(blame.commits.values(), (c, i) => { if (c.isUncommitted) return; - const uri = GitProvider.toBlameUri(c, i + 1, commitCount, range); - c.lines.forEach(l => locations.push(new Location(c.originalFileName - ? GitProvider.toBlameUri(c, i + 1, commitCount, range, c.originalFileName) - : uri, - new Position(l.originalLine, 0)))); + const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; + const uri = c.originalFileName + ? GitProvider.toGitUri(c, i + 1, commitCount, c.originalFileName, decoration) + : GitProvider.toGitUri(c, i + 1, commitCount, undefined, decoration); + locations.push(new Location(uri, new Position(0, 0))); + if (c.sha === selectedSha) { + locations.push(new Location(uri, new Position(line + 1, 0))); + } }); return locations; } - getLogForFile(fileName: string, range?: Range): Promise { - Logger.log(`getLogForFile('${fileName}', ${range})`); + getLogForFile(fileName: string, sha?: string, repoPath?: string, range?: Range): Promise { + Logger.log(`getLogForFile('${fileName}', ${sha}, ${repoPath}, ${range})`); fileName = Git.normalizePath(fileName); const useCaching = this.UseCaching && !range; @@ -377,7 +363,9 @@ export default class GitProvider extends Disposable { return >GitProvider.EmptyPromise; } - return (range ? Git.logRange(fileName, range.start.line + 1, range.end.line + 1) : Git.log(fileName)) + return (range + ? Git.logRange(fileName, range.start.line + 1, range.end.line + 1, sha, repoPath) + : Git.log(fileName, sha, repoPath)) .then(data => new GitLogParserEnricher().enrich(data, fileName)) .catch(ex => { // Trap and cache expected blame errors @@ -412,10 +400,10 @@ export default class GitProvider extends Disposable { return promise; } - async getLogLocations(fileName: string, sha?: string, line?: number): Promise { - Logger.log(`getLogLocations('${fileName}', ${sha}, ${line})`); + async getLogLocations(fileName: string, sha?: string, repoPath?: string, selectedSha?: string, line?: number): Promise { + Logger.log(`getLogLocations('${fileName}', ${sha}, ${repoPath}, ${selectedSha}, ${line})`); - const log = await this.getLogForFile(fileName); + const log = await this.getLogForFile(fileName, sha, repoPath); if (!log) return undefined; const commitCount = log.commits.size; @@ -423,12 +411,13 @@ export default class GitProvider extends Disposable { const locations: Array = []; Iterables.forEach(log.commits.values(), (c, i) => { if (c.isUncommitted) return; + const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`; const uri = c.originalFileName ? GitProvider.toGitUri(c, i + 1, commitCount, c.originalFileName, decoration) : GitProvider.toGitUri(c, i + 1, commitCount, undefined, decoration); locations.push(new Location(uri, new Position(0, 0))); - if (c.sha === sha) { + if (c.sha === selectedSha) { locations.push(new Location(uri, new Position(line + 1, 0))); } }); @@ -481,14 +470,6 @@ export default class GitProvider extends Disposable { return Git.isUncommitted(sha); } - static fromBlameUri(uri: Uri): IGitBlameUriData { - if (uri.scheme !== DocumentSchemes.GitBlame) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); - const data = GitProvider._fromGitUri(uri); - const range = data.range as Position[]; - data.range = new Range(range[0].line, range[0].character, range[1].line, range[1].character); - return data; - } - static fromGitUri(uri: Uri): IGitUriData { if (uri.scheme !== DocumentSchemes.Git) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); return GitProvider._fromGitUri(uri); @@ -498,15 +479,11 @@ export default class GitProvider extends Disposable { return JSON.parse(uri.query) as T; } - static toBlameUri(commit: GitCommit, index: number, commitCount: number, range: Range, originalFileName?: string) { - return GitProvider._toGitUri(commit, DocumentSchemes.GitBlame, commitCount, GitProvider._toGitBlameUriData(commit, index, range, originalFileName)); - } - static toGitUri(commit: GitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string) { return GitProvider._toGitUri(commit, DocumentSchemes.Git, commitCount, GitProvider._toGitUriData(commit, index, originalFileName, decoration)); } - private static _toGitUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData | IGitBlameUriData) { + private static _toGitUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData) { const pad = (n: number) => ('0000000' + n).slice(-('' + commitCount).length); const ext = path.extname(data.fileName); // const uriPath = `${dirname(data.fileName)}/${commit.sha}: ${basename(data.fileName, ext)}${ext}`; @@ -532,12 +509,6 @@ export default class GitProvider extends Disposable { } return data; } - - private static _toGitBlameUriData(commit: GitCommit, index: number, range: Range, originalFileName?: string) { - const data = this._toGitUriData(commit, index, originalFileName); - data.range = range; - return data; - } } export class GitUri extends Uri { @@ -557,7 +528,7 @@ export class GitUri extends Uri { base._fragment = uri.fragment; this.offset = 0; - if (uri.scheme === DocumentSchemes.Git || uri.scheme === DocumentSchemes.GitBlame) { + if (uri.scheme === DocumentSchemes.Git) { const data = GitProvider.fromGitUri(uri); base._fsPath = data.originalFileName || data.fileName; @@ -583,8 +554,4 @@ export interface IGitUriData { sha: string; index: number; decoration?: string; -} - -export interface IGitBlameUriData extends IGitUriData { - range: Range; } \ No newline at end of file diff --git a/src/gitRevisionCodeLensProvider.ts b/src/gitRevisionCodeLensProvider.ts new file mode 100644 index 0000000..87c83f0 --- /dev/null +++ b/src/gitRevisionCodeLensProvider.ts @@ -0,0 +1,75 @@ +'use strict'; +import { Iterables } from './system'; +import { CancellationToken, CodeLens, CodeLensProvider, DocumentSelector, ExtensionContext, Range, TextDocument, Uri } from 'vscode'; +import { Commands, DocumentSchemes } from './constants'; +import GitProvider, { GitCommit, GitUri } from './gitProvider'; + +export class GitDiffWithWorkingCodeLens extends CodeLens { + constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { + super(range); + } +} + +export class GitDiffWithPreviousCodeLens extends CodeLens { + constructor(git: GitProvider, public fileName: string, public commit: GitCommit, range: Range) { + super(range); + } +} + +export default class GitRevisionCodeLensProvider implements CodeLensProvider { + static selector: DocumentSelector = { scheme: DocumentSchemes.Git }; + + constructor(context: ExtensionContext, private git: GitProvider) { } + + async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const gitUri = GitUri.fromUri(document.uri); + + const lenses: CodeLens[] = []; + + const log = await this.git.getLogForFile(gitUri.fsPath, gitUri.sha, gitUri.repoPath); + if (!log) return lenses; + + const commit = (gitUri.sha && log.commits.get(gitUri.sha)) || Iterables.first(log.commits.values()); + if (!commit) return lenses; + + lenses.push(new GitDiffWithWorkingCodeLens(this.git, commit.uri.fsPath, commit, new Range(0, 0, 0, 1))); + + if (commit.previousSha) { + lenses.push(new GitDiffWithPreviousCodeLens(this.git, commit.previousUri.fsPath, commit, new Range(0, 1, 0, 2))); + } + + return lenses; + } + + resolveCodeLens(lens: CodeLens, token: CancellationToken): Thenable { + if (lens instanceof GitDiffWithWorkingCodeLens) return this._resolveDiffWithWorkingTreeCodeLens(lens, token); + if (lens instanceof GitDiffWithPreviousCodeLens) return this._resolveGitDiffWithPreviousCodeLens(lens, token); + return Promise.reject(undefined); + } + + _resolveDiffWithWorkingTreeCodeLens(lens: GitDiffWithWorkingCodeLens, token: CancellationToken): Thenable { + lens.command = { + title: `Compare (${lens.commit.sha}) with Working Tree`, + command: Commands.DiffWithWorking, + arguments: [ + Uri.file(lens.fileName), + lens.commit, + lens.range.start.line + ] + }; + return Promise.resolve(lens); + } + + _resolveGitDiffWithPreviousCodeLens(lens: GitDiffWithPreviousCodeLens, token: CancellationToken): Thenable { + lens.command = { + title: `Compare (${lens.commit.sha}) with Previous (${lens.commit.previousSha})`, + command: Commands.DiffWithPrevious, + arguments: [ + Uri.file(lens.fileName), + lens.commit, + lens.range.start.line + ] + }; + return Promise.resolve(lens); + } +}