From e70dd6d8c7d8715170da3ff705d5f7c11891369c Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 30 Sep 2017 00:43:02 -0400 Subject: [PATCH] Closes #148 - Adds custom provider support --- README.md | 4 +- package.json | 62 ++++++++++++++++++++++++++++- src/configuration.ts | 14 +++++++ src/git/remotes/bitbucket-server.ts | 4 +- src/git/remotes/bitbucket.ts | 4 +- src/git/remotes/custom.ts | 48 ++++++++++++++++++++++ src/git/remotes/factory.ts | 20 +++++----- src/git/remotes/github.ts | 4 +- src/git/remotes/gitlab.ts | 4 +- src/git/remotes/provider.ts | 17 +++++++- src/git/remotes/visualStudio.ts | 4 +- 11 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 src/git/remotes/custom.ts diff --git a/README.md b/README.md index 219ff2ffbe62a..3361f56502dd2 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, - Adds a `Search Commits` command (`gitlens.showCommitSearch`) with a shortcut of `alt+/` to search for commits by message, author, file(s), or commit id -- Adds commands to open files, commits, branches, and the repository in the supported remote services, currently **BitBucket, GitHub, GitLab, and Visual Studio Team Services** — only available if a Git upstream service is configured in the repository +- Adds commands to open files, commits, branches, and the repository in the supported remote services, **BitBucket, GitHub, GitLab, and Visual Studio Team Services** or a [**user-defined** remote services](#custom-remotes-settings) — only available if a Git upstream service is configured in the repository - Also supports [remote services with custom domains](#custom-remotes-settings), such as **BitBucket, Bitbucket Server (previously called Stash), GitHub, GitHub Enterprise, GitLab** - `Open Branches in Remote` command (`gitlens.openBranchesInRemote`) — opens the branches in the supported remote service - `Open Branch in Remote` command (`gitlens.openBranchInRemote`) — opens the current branch commits in the supported remote service @@ -371,7 +371,7 @@ GitLens is highly customizable and provides many configuration settings to allow |Name | Description |-----|------------ -|`gitlens.remotes`|Specifies any custom domains for remote (code-hosting) services
Example: ```"gitlens.remotes": [{ "domain": "git.corporate-url.com", "type": "GitHub" }]``` +|`gitlens.remotes`|Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services

Example:
```"gitlens.remotes": [{ "domain": "git.corporate-url.com", "type": "GitHub" }]```

Example:
```"gitlens.remotes": [{ "domain": "git.corporate-url.com", "type": "Custom", "name": "My Company", "urls": { "repository": "https://git.corporate-url.com/${repo}", "branches": "https://git.corporate-url.com/${repo}/branches", "branch": "https://git.corporate-url.com/${repo}/commits/${branch}", "commit": "https://git.corporate-url.com/${repo}/commit/${id}", "file": "https://git.corporate-url.com/${repo}?path=${file}${line}", "fileInBranch": "https://git.corporate-url.com/${repo}/blob/${branch}/${file}${line}", "fileInCommit": "https://git.corporate-url.com/${repo}/blob/${id}/${file}${line}", "fileLine": "#L${line}", "fileRange": "#L${start}-L${end}" } }``` ### Status Bar Settings diff --git a/package.json b/package.json index 4d5e7c05886cf..45e9f12582d93 100644 --- a/package.json +++ b/package.json @@ -498,6 +498,7 @@ "enum": [ "Bitbucket", "BitbucketServer", + "Custom", "GitHub", "GitLab" ], @@ -506,11 +507,68 @@ "domain": { "type": "string", "description": "Specifies the domain name of the custom remote service" - } + }, + "name": { + "type": "string", + "description": "Specifies an optional friendly name for the custom remote service" + }, + "urls": { + "type": "object", + "required": [ + "repository", + "branches", + "branch", + "commit", + "file", + "fileInCommit", + "fileInBranch", + "fileLine", + "fileRange" + ], + "properties": { + "repository": { + "type": "string", + "description": "Specifies the format of a respository url for the custom remote service\nAvailable tokens\n ${repo} - repository path" + }, + "branches": { + "type": "string", + "description": "Specifies the format of a branches url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${branch} - branch" + }, + "branch": { + "type": "string", + "description": "Specifies the format of a branch url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${branch} - branch" + }, + "commit": { + "type": "string", + "description": "Specifies the format of a commit url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${id} - commit id" + }, + "file": { + "type": "string", + "description": "Specifies the format of a file url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${file} - file name\n ${line} - formatted line information" + }, + "fileInBranch": { + "type": "string", + "description": "Specifies the format of a branch file url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${file} - file name\n ${branch} - branch\n ${line} - formatted line information" + }, + "fileInCommit": { + "type": "string", + "description": "Specifies the format of a commit file url for the custom remote service\nAvailable tokens\n ${repo} - repository path\n ${file} - file name\n ${id} - commit id\n ${line} - formatted line information" + }, + "fileLine": { + "type": "string", + "description": "Specifies the format of a line in a file url for the custom remote service\nAvailable tokens\n ${line} - line" + }, + "fileRange": { + "type": "string", + "description": "Specifies the format of a range in a file url for the custom remote service\nAvailable tokens\n ${start} - starting line\n ${end} - ending line" + } + } + }, + "description": "Specifies the url formats of the custom remote service" } }, "uniqueItems": true, - "description": "Specifies any custom domains for remote (code-hosting) services" + "description": "Specifies user-defined remote (code-hosting) services or custom domains for built-in remote services" }, "gitlens.statusBar.enabled": { "type": "boolean", diff --git a/src/configuration.ts b/src/configuration.ts index 15a9eb44da51a..e3644fa0d0d57 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -44,11 +44,13 @@ export const LineHighlightLocations = { export type CustomRemoteType = 'Bitbucket' | + 'Custom' | 'GitHub' | 'GitLab'; export const CustomRemoteType = { Bitbucket: 'Bitbucket' as CustomRemoteType, BitbucketServer: 'BitbucketServer' as CustomRemoteType, + Custom: 'Custom' as CustomRemoteType, GitHub: 'GitHub' as CustomRemoteType, GitLab: 'GitLab' as CustomRemoteType }; @@ -163,6 +165,18 @@ export interface IGitExplorerConfig { export interface IRemotesConfig { type: CustomRemoteType; domain: string; + name?: string; + urls?: { + repository: string; + branches: string; + branch: string; + commit: string; + file: string; + fileInBranch: string; + fileInCommit: string; + fileLine: string; + fileRange: string; + }; } export interface IThemeConfig { diff --git a/src/git/remotes/bitbucket-server.ts b/src/git/remotes/bitbucket-server.ts index b808be25b38cd..2b25c8a8e01c6 100644 --- a/src/git/remotes/bitbucket-server.ts +++ b/src/git/remotes/bitbucket-server.ts @@ -4,8 +4,8 @@ import { RemoteProvider } from './provider'; export class BitbucketServerService extends RemoteProvider { - constructor(public domain: string, public path: string, public custom: boolean = false) { - super(domain, path); + constructor(domain: string, path: string, name?: string, custom: boolean = false) { + super(domain, path, name, custom); } get name() { diff --git a/src/git/remotes/bitbucket.ts b/src/git/remotes/bitbucket.ts index 9cf8f6e839c89..c6accbe29171c 100644 --- a/src/git/remotes/bitbucket.ts +++ b/src/git/remotes/bitbucket.ts @@ -4,8 +4,8 @@ import { RemoteProvider } from './provider'; export class BitbucketService extends RemoteProvider { - constructor(public domain: string, public path: string, public custom: boolean = false) { - super(domain, path); + constructor(domain: string, path: string, name?: string, custom: boolean = false) { + super(domain, path, name, custom); } get name() { diff --git a/src/git/remotes/custom.ts b/src/git/remotes/custom.ts new file mode 100644 index 0000000000000..88e801c616eb2 --- /dev/null +++ b/src/git/remotes/custom.ts @@ -0,0 +1,48 @@ +'use strict'; +import { Strings } from '../../system'; +import { Range } from 'vscode'; +import { IRemotesConfig } from '../../configuration'; +import { RemoteProvider } from './provider'; + +export class CustomService extends RemoteProvider { + + constructor(domain: string, path: string, private readonly config: IRemotesConfig) { + super(domain, path, config.name, true); + } + + get name() { + return this.formatName('Custom'); + } + + protected getUrlForRepository(): string { + return Strings.interpolate(this.config.custom!.repository, { repo: this.path }); + } + + protected getUrlForBranches(): string { + return Strings.interpolate(this.config.custom!.branches, { repo: this.path }); + } + + protected getUrlForBranch(branch: string): string { + return Strings.interpolate(this.config.custom!.branch, { repo: this.path, branch: branch }); + } + + protected getUrlForCommit(sha: string): string { + return Strings.interpolate(this.config.custom!.commit, { repo: this.path, id: sha }); + } + + protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string { + let line = ''; + if (range) { + if (range.start.line === range.end.line) { + line = Strings.interpolate(this.config.custom!.fileLine, { line: range.start.line }); + } + else { + line = Strings.interpolate(this.config.custom!.fileRange, { start: range.start.line, end: range.end.line }); + } + } + + if (sha) return Strings.interpolate(this.config.custom!.fileInCommit, { repo: this.path, id: sha, file: fileName, line: line }); + if (branch) return Strings.interpolate(this.config.custom!.fileInBranch, { repo: this.path, branch: branch, file: fileName, line: line }); + return Strings.interpolate(this.config.custom!.file, { repo: this.path, file: fileName, line: line }); + } +} \ No newline at end of file diff --git a/src/git/remotes/factory.ts b/src/git/remotes/factory.ts index 083d7019f300d..bd73a65af16fd 100644 --- a/src/git/remotes/factory.ts +++ b/src/git/remotes/factory.ts @@ -5,6 +5,7 @@ import { BitbucketService } from './bitbucket'; import { BitbucketServerService } from './bitbucket-server'; import { CustomRemoteType, IConfig, IRemotesConfig } from '../../configuration'; import { ExtensionKey } from '../../constants'; +import { CustomService } from './custom'; import { GitHubService } from './github'; import { GitLabService } from './gitlab'; import { Logger } from '../../logger'; @@ -62,11 +63,11 @@ export class RemoteProviderFactory { this._remotesCfg = cfg.remotes; if (this._remotesCfg != null && this._remotesCfg.length > 0) { - for (const svc of this._remotesCfg) { - const provider = this.getCustomProvider(svc.type); + for (const remoteCfg of this._remotesCfg) { + const provider = this.getCustomProvider(remoteCfg); if (provider === undefined) continue; - this._providerMap.set(svc.domain.toLowerCase(), provider); + this._providerMap.set(remoteCfg.domain.toLowerCase(), provider); } if (!silent) { @@ -76,12 +77,13 @@ export class RemoteProviderFactory { } } - private static getCustomProvider(type: CustomRemoteType) { - switch (type) { - case CustomRemoteType.Bitbucket: return (domain: string, path: string) => new BitbucketService(domain, path, true); - case CustomRemoteType.BitbucketServer: return (domain: string, path: string) => new BitbucketServerService(domain, path, true); - case CustomRemoteType.GitHub: return (domain: string, path: string) => new GitHubService(domain, path, true); - case CustomRemoteType.GitLab: return (domain: string, path: string) => new GitLabService(domain, path, true); + private static getCustomProvider(cfg: IRemotesConfig) { + switch (cfg.type) { + case CustomRemoteType.Bitbucket: return (domain: string, path: string) => new BitbucketService(domain, path, cfg.name, true); + case CustomRemoteType.BitbucketServer: return (domain: string, path: string) => new BitbucketServerService(domain, path, cfg.name, true); + case CustomRemoteType.Custom: return (domain: string, path: string) => new CustomService(domain, path, cfg); + case CustomRemoteType.GitHub: return (domain: string, path: string) => new GitHubService(domain, path, cfg.name, true); + case CustomRemoteType.GitLab: return (domain: string, path: string) => new GitLabService(domain, path, cfg.name, true); } return undefined; } diff --git a/src/git/remotes/github.ts b/src/git/remotes/github.ts index 7d8b8ef711b8b..804d9b84134c5 100644 --- a/src/git/remotes/github.ts +++ b/src/git/remotes/github.ts @@ -4,8 +4,8 @@ import { RemoteProvider } from './provider'; export class GitHubService extends RemoteProvider { - constructor(public domain: string, public path: string, public custom: boolean = false) { - super(domain, path); + constructor(domain: string, path: string, name?: string, custom: boolean = false) { + super(domain, path, name, custom); } get name() { diff --git a/src/git/remotes/gitlab.ts b/src/git/remotes/gitlab.ts index a7efa11fe4ab9..7062846079082 100644 --- a/src/git/remotes/gitlab.ts +++ b/src/git/remotes/gitlab.ts @@ -4,8 +4,8 @@ import { RemoteProvider } from './provider'; export class GitLabService extends RemoteProvider { - constructor(public domain: string, public path: string, public custom: boolean = false) { - super(domain, path); + constructor(domain: string, path: string, name?: string, custom: boolean = false) { + super(domain, path, name, custom); } get name() { diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index 852e015c77b59..f98aec4882264 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -26,7 +26,16 @@ export function getNameFromRemoteResource(resource: RemoteResource) { export abstract class RemoteProvider { - constructor(public domain: string, public path: string, public custom: boolean = false) { } + private _name: string | undefined; + + constructor( + public readonly domain: string, + public readonly path: string, + name?: string, + public readonly custom: boolean = false + ) { + this._name = name; + } abstract get name(): string; @@ -35,6 +44,7 @@ export abstract class RemoteProvider { } protected formatName(name: string) { + if (this._name !== undefined) return this._name; return `${name}${this.custom ? ` (${this.domain})` : ''}`; } @@ -43,6 +53,9 @@ export abstract class RemoteProvider { return [ this.path.substring(0, index), this.path.substring(index + 1) ]; } + protected getUrlForRepository(): string { + return this.baseUrl; + } protected abstract getUrlForBranches(): string; protected abstract getUrlForBranch(branch: string): string; protected abstract getUrlForCommit(sha: string): string; @@ -66,7 +79,7 @@ export abstract class RemoteProvider { } openRepo() { - return this._openUrl(this.baseUrl); + return this._openUrl(this.getUrlForRepository()); } openBranches() { diff --git a/src/git/remotes/visualStudio.ts b/src/git/remotes/visualStudio.ts index f87d29af73b5b..d28fd43d0b007 100644 --- a/src/git/remotes/visualStudio.ts +++ b/src/git/remotes/visualStudio.ts @@ -4,8 +4,8 @@ import { RemoteProvider } from './provider'; export class VisualStudioService extends RemoteProvider { - constructor(public domain: string, public path: string) { - super(domain, path); + constructor(domain: string, path: string, name?: string) { + super(domain, path, name); } get name() {