diff --git a/package.json b/package.json index 955067d..cc2819b 100644 --- a/package.json +++ b/package.json @@ -354,7 +354,7 @@ "gitlens.experimental.openInHostingProvider": { "type": "boolean", "default": false, - "description": "(experimental) Specifies whether or not to show Open X in Hosting Provider commands in quick picks" + "description": "(experimental) Specifies whether or not to show Open X in Hosting Provider commands in relevant quick picks" } } }, @@ -477,6 +477,16 @@ "command": "gitlens.openChangedFiles", "title": "Open Changed Files", "category": "GitLens" + }, + { + "command": "gitlens.openCommitInHostingProvider", + "title": "Open Line Commit in Hosting Provider", + "category": "GitLens" + }, + { + "command": "gitlens.openFileInHostingProvider", + "title": "Open File in Hosting Provider", + "category": "GitLens" } ], "menus": { @@ -572,6 +582,14 @@ { "command": "gitlens.openChangedFiles", "when": "gitlens:enabled" + }, + { + "command": "gitlens.openCommitInHostingProvider", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable && config.gitlens.experimental.openInHostingProvider" + }, + { + "command": "gitlens.openFileInHostingProvider", + "when": "editorTextFocus && gitlens:enabled && config.gitlens.experimental.openInHostingProvider" } ], "explorer/context": [ diff --git a/src/commands.ts b/src/commands.ts index d9fcc39..27b292e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -16,6 +16,9 @@ export * from './commands/diffWithNext'; export * from './commands/diffWithPrevious'; export * from './commands/diffWithWorking'; export * from './commands/openChangedFiles'; +export * from './commands/openCommitInHostingProvider'; +export * from './commands/openFileInHostingProvider'; +export * from './commands/openInHostingProvider'; export * from './commands/showBlame'; export * from './commands/showBlameHistory'; export * from './commands/showFileHistory'; diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 52b4995..8985f38 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -2,7 +2,15 @@ import { commands, Disposable, TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { BuiltInCommands } from '../constants'; -export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | 'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.openChangedFiles' | 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | 'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' | 'gitlens.showQuickRepoStatus' | 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; +export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | + 'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | + 'gitlens.openChangedFiles' | 'gitlens.openCommitInHostingProvider' | 'gitlens.openFileInHostingProvider' | 'gitlens.openInHostingProvider' | + 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | + 'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' | + 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | + 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' | + 'gitlens.showQuickRepoStatus' | + 'gitlens.toggleBlame' | 'gitlens.toggleCodeLens'; export const Commands = { CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands, CopyMessageToClipboard: 'gitlens.copyMessageToClipboard' as Commands, @@ -15,6 +23,9 @@ export const Commands = { DiffWithWorking: 'gitlens.diffWithWorking' as Commands, DiffLineWithWorking: 'gitlens.diffLineWithWorking' as Commands, OpenChangedFiles: 'gitlens.openChangedFiles' as Commands, + OpenCommitInHostingProvider: 'gitlens.openCommitInHostingProvider' as Commands, + OpenFileInHostingProvider: 'gitlens.openFileInHostingProvider' as Commands, + OpenInHostingProvider: 'gitlens.openInHostingProvider' as Commands, ShowBlame: 'gitlens.showBlame' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, ShowFileHistory: 'gitlens.showFileHistory' as Commands, diff --git a/src/commands/openCommitInHostingProvider.ts b/src/commands/openCommitInHostingProvider.ts new file mode 100644 index 0000000..714e4cd --- /dev/null +++ b/src/commands/openCommitInHostingProvider.ts @@ -0,0 +1,46 @@ +'use strict'; +import { Arrays } from '../system'; +import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands } from './commands'; +import { GitCommit, GitService, GitUri } from '../gitService'; +import { Logger } from '../logger'; + +export class OpenCommitInHostingProviderCommand extends ActiveEditorCommand { + + constructor(private git: GitService, private repoPath: string) { + super(Commands.OpenCommitInHostingProvider); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri) { + if (!(uri instanceof Uri)) { + if (!editor || !editor.document) return undefined; + uri = editor.document.uri; + } + + if (editor && editor.document && editor.document.isDirty) return undefined; + + const gitUri = await GitUri.fromUri(uri, this.git); + const line = (editor && editor.selection.active.line) || gitUri.offset; + + try { + const blameline = line - gitUri.offset; + if (blameline < 0) return undefined; + + const blame = await this.git.getBlameForLine(gitUri, blameline); + if (!blame) return window.showWarningMessage(`Unable to open commit in hosting provider. File is probably not under source control`); + + let commit = blame.commit; + // If the line is uncommitted, find the previous commit + if (commit.isUncommitted) { + commit = new GitCommit(commit.repoPath, commit.previousSha, commit.previousFileName, commit.author, commit.date, commit.message); + } + + const remotes = Arrays.uniqueBy(await this.git.getRemotes(this.repoPath), _ => _.url, _ => !!_.provider); + return commands.executeCommand(Commands.OpenInHostingProvider, uri, remotes, 'commit', [commit.sha]); + } + catch (ex) { + Logger.error('[GitLens.OpenCommitInHostingProviderCommand]', ex); + return window.showErrorMessage(`Unable to open commit in hosting provider. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/openFileInHostingProvider.ts b/src/commands/openFileInHostingProvider.ts new file mode 100644 index 0000000..793f24c --- /dev/null +++ b/src/commands/openFileInHostingProvider.ts @@ -0,0 +1,31 @@ +'use strict'; +import { Arrays } from '../system'; +import { commands, TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands } from './commands'; +import { GitService, GitUri } from '../gitService'; +import { Logger } from '../logger'; + +export class OpenFileInHostingProviderCommand extends ActiveEditorCommand { + + constructor(private git: GitService, private repoPath: string) { + super(Commands.OpenFileInHostingProvider); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri) { + if (!(uri instanceof Uri)) { + if (!editor || !editor.document) return undefined; + uri = editor.document.uri; + } + + const gitUri = await GitUri.fromUri(uri, this.git); + + try { + const remotes = Arrays.uniqueBy(await this.git.getRemotes(this.repoPath), _ => _.url, _ => !!_.provider); + return commands.executeCommand(Commands.OpenInHostingProvider, uri, remotes, 'file', [gitUri.getRelativePath(), gitUri.sha]); + } + catch (ex) { + Logger.error('[GitLens.OpenFileInHostingProviderCommand]', ex); + return window.showErrorMessage(`Unable to open file in hosting provider. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/openInHostingProvider.ts b/src/commands/openInHostingProvider.ts new file mode 100644 index 0000000..068787c --- /dev/null +++ b/src/commands/openInHostingProvider.ts @@ -0,0 +1,53 @@ +'use strict'; +import { TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCommand, Commands } from './commands'; +import { GitRemote, HostingProviderOpenType } from '../gitService'; +import { Logger } from '../logger'; +import { CommandQuickPickItem, OpenRemoteCommandQuickPickItem, RemotesQuickPick } from '../quickPicks'; + +export class OpenInHostingProviderCommand extends ActiveEditorCommand { + + constructor() { + super(Commands.OpenInHostingProvider); + } + + async execute(editor: TextEditor, uri?: Uri, remotes?: GitRemote[], type?: HostingProviderOpenType, args?: string[], name?: string, goBackCommand?: CommandQuickPickItem) { + if (!(uri instanceof Uri)) { + uri = editor && editor.document && editor.document.uri; + } + + try { + if (!remotes) return undefined; + + if (remotes.length === 1) { + const command = new OpenRemoteCommandQuickPickItem(remotes[0], type, ...args, name); + return command.execute(); + } + + let placeHolder: string; + switch (type) { + case 'branch': + placeHolder = `open ${args[0]} branch in\u2026`; + break; + case 'commit': + const shortSha = args[0].substring(0, 8); + placeHolder = `open commit ${shortSha} in\u2026`; + break; + case 'file': + const shortFileSha = (args[1] && args[1].substring(0, 8)) || ''; + const shaSuffix = shortFileSha ? ` \u00a0\u2022\u00a0 ${shortFileSha}` : ''; + + placeHolder = `open ${args[0]}${shaSuffix} in\u2026`; + break; + } + + const pick = await RemotesQuickPick.show(remotes, placeHolder, type, args, name, goBackCommand); + return pick && pick.execute(); + + } + catch (ex) { + Logger.error('[GitLens.OpenInHostingProviderCommand]', ex); + return window.showErrorMessage(`Unable to open in hosting provider. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 8f79d82..dd1f887 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { BlameAnnotationController } from './blameAnnotationController'; import { configureCssCharacters } from './blameAnnotationFormatter'; import { CommandContext, setCommandContext } from './commands'; import { CloseUnchangedFilesCommand, OpenChangedFilesCommand } from './commands'; +import { OpenCommitInHostingProviderCommand, OpenFileInHostingProviderCommand, OpenInHostingProviderCommand } from './commands'; import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; import { ShowBlameCommand, ToggleBlameCommand } from './commands'; @@ -101,6 +102,9 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new DiffWithNextCommand(git)); context.subscriptions.push(new DiffWithPreviousCommand(git)); context.subscriptions.push(new DiffWithWorkingCommand(git)); + context.subscriptions.push(new OpenCommitInHostingProviderCommand(git, repoPath)); + context.subscriptions.push(new OpenFileInHostingProviderCommand(git, repoPath)); + context.subscriptions.push(new OpenInHostingProviderCommand()); context.subscriptions.push(new ShowBlameCommand(annotationController)); context.subscriptions.push(new ToggleBlameCommand(annotationController)); context.subscriptions.push(new ShowBlameHistoryCommand(git)); diff --git a/src/quickPicks/remotes.ts b/src/quickPicks/remotes.ts index aa282ae..7ce6b87 100644 --- a/src/quickPicks/remotes.ts +++ b/src/quickPicks/remotes.ts @@ -1,5 +1,6 @@ 'use strict'; import { QuickPickOptions, window } from 'vscode'; +import { Commands } from '../commands'; import { GitRemote, HostingProviderOpenType } from '../gitService'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './quickPicks'; import * as path from 'path'; @@ -32,12 +33,6 @@ export class OpenRemoteCommandQuickPickItem extends CommandQuickPickItem { export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { - private goBackCommand: CommandQuickPickItem; - private name: string; - private placeHolder: string; - private remotes: GitRemote[]; - private type: HostingProviderOpenType; - constructor(remotes: GitRemote[], type: 'branch', branch: string, goBackCommand?: CommandQuickPickItem); constructor(remotes: GitRemote[], type: 'commit', sha: string, goBackCommand?: CommandQuickPickItem); constructor(remotes: GitRemote[], type: 'file', fileName: string, sha?: string, name?: string, goBackCommand?: CommandQuickPickItem); @@ -81,35 +76,19 @@ export class OpenRemotesCommandQuickPickItem extends CommandQuickPickItem { super({ label: `$(link-external) Open ${name} in ${remote.provider.name}`, description: `\u00a0 \u2014 \u00a0\u00a0 $(repo) ${remote.provider.path} \u00a0\u2022\u00a0 ${description}` - }, undefined, undefined); - } - else { - const provider = remotes.every(_ => _.provider.name === remote.provider.name) - ? remote.provider.name - : 'Hosting Provider'; + }, Commands.OpenInHostingProvider, [undefined, remotes, type, [branchOrShaOrFileName, fileSha], name, goBackCommand]); - super({ - label: `$(link-external) Open ${name} in ${provider}\u2026`, - description: `\u00a0 \u2014 \u00a0\u00a0 ${description}` - }, undefined, undefined); + return; } - this.goBackCommand = goBackCommand; - this.name = name; - this.placeHolder = placeHolder; - this.remotes = remotes; - this.type = type; - this.args = [branchOrShaOrFileName, fileSha]; - } + const provider = remotes.every(_ => _.provider.name === remote.provider.name) + ? remote.provider.name + : 'Hosting Provider'; - async execute(): Promise<{}> { - if (this.remotes.length === 1) { - const command = new OpenRemoteCommandQuickPickItem(this.remotes[0], this.type, ...this.args); - return command.execute(); - } - - const pick = await RemotesQuickPick.show(this.remotes, this.placeHolder, this.type, this.args, this.name, this.goBackCommand); - return pick && pick.execute(); + super({ + label: `$(link-external) Open ${name} in ${provider}\u2026`, + description: `\u00a0 \u2014 \u00a0\u00a0 ${description}` + }, Commands.OpenInHostingProvider, [undefined, remotes, type, [branchOrShaOrFileName, fileSha], name, goBackCommand]); } }