Просмотр исходного кода

Closes #897 - Adds autolink support

main
Eric Amodio 5 лет назад
Родитель
Сommit
59e7685454
14 измененных файлов: 283 добавлений и 76 удалений
  1. +4
    -0
      CHANGELOG.md
  2. +7
    -1
      README.md
  3. +34
    -0
      package.json
  4. +1
    -0
      src/annotations/annotations.ts
  5. +84
    -0
      src/annotations/autolinks.ts
  6. +9
    -0
      src/config.ts
  7. +10
    -0
      src/container.ts
  8. +11
    -10
      src/git/formatters/commitFormatter.ts
  9. +18
    -12
      src/git/remotes/azure-devops.ts
  10. +21
    -13
      src/git/remotes/bitbucket-server.ts
  11. +21
    -13
      src/git/remotes/bitbucket.ts
  12. +41
    -13
      src/git/remotes/github.ts
  13. +16
    -10
      src/git/remotes/gitlab.ts
  14. +6
    -4
      src/git/remotes/provider.ts

+ 4
- 0
CHANGELOG.md Просмотреть файл

@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added
- Adds user-defined autolinks to external resources in commit messages — closes [#897](https://github.com/eamodio/vscode-gitlens/issues/897)
- Adds a `gitlens.autolinks` setting to configure the autolinks
- For example to autolink Jira issues (e.g. `JIRA-123 ⟶ https://jira.company.com/issue?query=123`):
- Use `"gitlens.autolinks": [{ "prefix": "JIRA-", "url": "https://jira.company.com/issue?query=<num>" }]`
- Adds a _Highlight Changes_ command (`gitlens.views.highlightChanges`) to commits in GitLens views to highlight the changes lines in the current file
- Adds a _Highlight Revision Changes_ command (`gitlens.views.highlightRevisionChanges`) to commits in GitLens views to highlight the changes lines in the revision
- Adds branch and tag sorting options to the interactive settings editor

+ 7
- 1
README.md Просмотреть файл

@ -884,7 +884,13 @@ See also [View Settings](#view-settings- 'Jump to the View settings')
| `gitlens.mode.statusBar.alignment` | Specifies the active GitLens mode alignment in the status bar<br /><br />`left` - aligns to the left<br />`right` - aligns to the right |
| `gitlens.modes` | Specifies the user-defined GitLens modes<br /><br />Example &mdash; adds heatmap annotations to the built-in _Reviewing_ mode<br />`"gitlens.modes": { "review": { "annotations": "heatmap" } }`<br /><br />Example &mdash; adds a new _Annotating_ mode with blame annotations<br />`"gitlens.modes": {`<br />&nbsp;&nbsp;&nbsp;&nbsp;`"annotate": {`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"name": "Annotating",`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"statusBarItemName": "Annotating",`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"description": "for root cause analysis",`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"annotations": "blame",`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"codeLens": false,`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"currentLine": false,`<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`"hovers": true`<br />&nbsp;&nbsp;&nbsp;&nbsp;`}`<br />`}` |
#### Custom Remotes Settings
### Autolink Settings
| Name | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `gitlens.autolinks` | Specifies autolinks to external resources in commit messages. Use `<num>` as the variable for the reference number<br /><br />Example to autolink Jira issues: (e.g. `JIRA-123 ⟶ https://jira.company.com/issue?query=123`)<br />`"gitlens.autolinks": [{ "prefix": "JIRA-", "url": "https://jira.company.com/issue?query=<num>" }]` |
### Custom Remotes Settings
| Name | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

+ 34
- 0
package.json Просмотреть файл

@ -53,6 +53,40 @@
"type": "object",
"title": "GitLens — Use 'GitLens: Open Settings' for a richer, interactive experience",
"properties": {
"gitlens.autolinks": {
"type": "array",
"items": {
"type": "object",
"required": [
"prefix",
"url"
],
"properties": {
"prefix": {
"type": "string",
"description": "Specifies the short prefix to use to generate autolinks for the external resource"
},
"ignoreCase": {
"type": "boolean",
"description": "Specifies whether case should be ignored when matching the prefix",
"default": false
},
"title": {
"type": "string",
"description": "Specifies an optional title for the generated autolink. Use `<num>` as the variable for the reference number",
"default": null
},
"url": {
"type": "string",
"description": "Specifies the url of the external resource you want to link to. Use `<num>` as the variable for the reference number"
}
},
"default": null
},
"uniqueItems": true,
"markdownDescription": "Specifies autolinks to external resources in commit messages. Use <num> as the variable for the reference number",
"scope": "window"
},
"gitlens.blame.avatars": {
"type": "boolean",
"default": true,

+ 1
- 0
src/annotations/annotations.ts Просмотреть файл

@ -1,3 +1,4 @@
'use strict';
import {
DecorationInstanceRenderOptions,
DecorationOptions,

+ 84
- 0
src/annotations/autolinks.ts Просмотреть файл

@ -0,0 +1,84 @@
'use strict';
import { ConfigurationChangeEvent, Disposable } from 'vscode';
import { AutolinkReference, configuration } from '../configuration';
import { Container } from '../container';
import { Strings } from '../system';
import { Logger } from '../logger';
import { GitRemote } from '../git/git';
const numRegex = /<num>/g;
export interface DynamicAutolinkReference {
linkify: (text: string) => string;
}
function requiresGenerator(ref: AutolinkReference | DynamicAutolinkReference): ref is AutolinkReference {
return ref.linkify === undefined;
}
export class Autolinks implements Disposable {
protected _disposable: Disposable | undefined;
private _references: AutolinkReference[] = [];
constructor() {
this._disposable = Disposable.from(configuration.onDidChange(this.onConfigurationChanged, this));
this.onConfigurationChanged(configuration.initializingChangeEvent);
}
dispose() {
this._disposable && this._disposable.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'autolinks')) {
this._references = Container.config.autolinks ?? [];
}
}
linkify(text: string, remotes?: GitRemote[]) {
for (const ref of this._references) {
if (requiresGenerator(ref)) {
ref.linkify = this._getAutolinkGenerator(ref);
}
if (ref.linkify != null) {
text = ref.linkify(text);
}
}
if (remotes !== undefined) {
for (const r of remotes) {
if (r.provider === undefined) continue;
for (const ref of this._references) {
if (requiresGenerator(ref)) {
ref.linkify = this._getAutolinkGenerator(ref);
}
if (ref.linkify != null) {
text = ref.linkify(text);
}
}
}
}
return text;
}
private _getAutolinkGenerator({ prefix, url, title }: AutolinkReference) {
try {
const regex = new RegExp(
`(?<=^|\\s)(${Strings.escapeMarkdown(prefix).replace(/\\/g, '\\\\')}([0-9]+))\\b`,
'g'
);
const markdown = `[$1](${url.replace(numRegex, '$2')}${
title ? ` "${title.replace(numRegex, '$2')}"` : ''
})`;
return (text: string) => text.replace(regex, markdown);
} catch (ex) {
Logger.error(ex, `Failed to create autolink generator: prefix=${prefix}, url=${url}, title=${title}`);
return null;
}
}
}

+ 9
- 0
src/config.ts Просмотреть файл

@ -2,6 +2,7 @@
import { TraceLevel } from './logger';
export interface Config {
autolinks: AutolinkReference[] | null;
blame: {
avatars: boolean;
compact: boolean;
@ -117,6 +118,14 @@ export enum AnnotationsToggleMode {
Window = 'window'
}
export interface AutolinkReference {
prefix: string;
url: string;
title?: string;
ignoreCase?: boolean;
linkify?: ((text: string) => string) | null;
}
export enum BranchSorting {
NameDesc = 'name:desc',
NameAsc = 'name:asc',

+ 10
- 0
src/container.ts Просмотреть файл

@ -1,5 +1,6 @@
'use strict';
import { commands, ConfigurationChangeEvent, Disposable, ExtensionContext, Uri } from 'vscode';
import { Autolinks } from './annotations/autolinks';
import { FileAnnotationController } from './annotations/fileAnnotationController';
import { LineAnnotationController } from './annotations/lineAnnotationController';
import { GitCodeLensController } from './codelens/codeLensController';
@ -141,6 +142,15 @@ export class Container {
}
}
private static _autolinks: Autolinks;
static get autolinks() {
if (this._autolinks === undefined) {
this._context.subscriptions.push((this._autolinks = new Autolinks()));
}
return this._autolinks;
}
private static _codeLensController: GitCodeLensController;
static get codeLens() {
return this._codeLensController;

+ 11
- 10
src/git/formatters/commitFormatter.ts Просмотреть файл

@ -307,16 +307,7 @@ export class CommitFormatter extends Formatter {
return message;
}
message = Strings.escapeMarkdown(message, { quoted: true });
if (this._options.remotes !== undefined) {
for (const r of this._options.remotes) {
if (r.provider === undefined) continue;
message = r.provider.enrichMessage(message);
break;
}
}
message = Container.autolinks.linkify(Strings.escapeMarkdown(message, { quoted: true }), this._options.remotes);
return `\n> ${message}`;
}
@ -357,3 +348,13 @@ export class CommitFormatter extends Formatter {
return regex.test(format);
}
}
// const autolinks = new Autolinks();
// const text = autolinks.linkify(`\\#756
// foo
// bar
// baz \\#756
// boo\\#789
// \\#666
// gh\\-89 gh\\-89gh\\-89 GH\\-89`);
// console.log(text);

+ 18
- 12
src/git/remotes/azure-devops.ts Просмотреть файл

@ -1,8 +1,8 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
const issueEnricherRegex = /(^|\s)\\?(#([0-9]+))\b/gi;
import { AutolinkReference } from '../../config';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
const gitRegex = /\/_git\/?/i;
const legacyDefaultCollectionRegex = /^DefaultCollection\//i;
@ -34,6 +34,22 @@ export class AzureDevOpsRemote extends RemoteProvider {
super(domain, path, protocol, name);
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
// Strip off any `_git` part from the repo url
const baseUrl = this.baseUrl.replace(gitRegex, '/');
this._autolinks = [
{
prefix: '#',
url: `${baseUrl}/_workitems/edit/<num>`,
title: 'Open Work Item #<num>'
}
];
}
return this._autolinks;
}
get icon() {
return 'vsts';
}
@ -50,16 +66,6 @@ export class AzureDevOpsRemote extends RemoteProvider {
return this._displayPath;
}
enrichMessage(message: string): string {
// Strip off any `_git` part from the repo url
const baseUrl = this.baseUrl.replace(gitRegex, '/');
return (
message
// Matches #123
.replace(issueEnricherRegex, `$1[$2](${baseUrl}/_workitems/edit/$3 "Open Work Item $2")`)
);
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 21
- 13
src/git/remotes/bitbucket-server.ts Просмотреть файл

@ -1,15 +1,33 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
const issueEnricherRegex = /(^|\s)(issue \\?#([0-9]+))\b/gi;
const prEnricherRegex = /(^|\s)(pull request \\?#([0-9]+))\b/gi;
import { AutolinkReference } from '../../config';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
export class BitbucketServerRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
this._autolinks = [
{
prefix: 'issue #',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
},
{
prefix: 'pull request #',
url: `${this.baseUrl}/pull-requests/<num>`,
title: 'Open PR #<num>'
}
];
}
return this._autolinks;
}
protected get baseUrl() {
const [project, repo] = this.path.startsWith('scm/')
? this.path.replace('scm/', '').split('/')
@ -25,16 +43,6 @@ export class BitbucketServerRemote extends RemoteProvider {
return this.formatName('Bitbucket Server');
}
enrichMessage(message: string): string {
return (
message
// Matches issue #123
.replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
// Matches pull request #123
.replace(prEnricherRegex, `$1[$2](${this.baseUrl}/pull-requests/$3 "Open PR $2")`)
);
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 21
- 13
src/git/remotes/bitbucket.ts Просмотреть файл

@ -1,15 +1,33 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
const issueEnricherRegex = /(^|\s)(issue \\?#([0-9]+))\b/gi;
const prEnricherRegex = /(^|\s)(pull request \\?#([0-9]+))\b/gi;
import { AutolinkReference } from '../../config';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
export class BitbucketRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
this._autolinks = [
{
prefix: 'issue #',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
},
{
prefix: 'pull request #',
url: `${this.baseUrl}/pull-requests/<num>`,
title: 'Open PR #<num>'
}
];
}
return this._autolinks;
}
get icon() {
return 'bitbucket';
}
@ -18,16 +36,6 @@ export class BitbucketRemote extends RemoteProvider {
return this.formatName('Bitbucket');
}
enrichMessage(message: string): string {
return (
message
// Matches issue #123
.replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
// Matches pull request #123
.replace(prEnricherRegex, `$1[$2](${this.baseUrl}/pull-requests/$3 "Open PR $2")`)
);
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 41
- 13
src/git/remotes/github.ts Просмотреть файл

@ -1,8 +1,9 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
import { AutolinkReference } from '../../config';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
const issueEnricherRegex = /(^|\s)((?:\\?#|gh\\?-)([0-9]+))\b/gi;
const issueEnricher3rdParyRegex = /\b(\w+\\?-?\w+(?!\\?-)\/\w+\\?-?\w+(?!\\?-))\\?#([0-9]+)\b/g;
export class GitHubRemote extends RemoteProvider {
@ -10,6 +11,33 @@ export class GitHubRemote extends RemoteProvider {
super(domain, path, protocol, name, custom);
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
this._autolinks = [
{
prefix: '#',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
},
{
prefix: 'gh-',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>',
ignoreCase: true
},
{
linkify: (text: string) =>
text.replace(
issueEnricher3rdParyRegex,
`[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1")`
)
}
];
}
return this._autolinks;
}
get icon() {
return 'github';
}
@ -18,18 +46,18 @@ export class GitHubRemote extends RemoteProvider {
return this.formatName('GitHub');
}
enrichMessage(message: string): string {
return (
message
// Matches #123 or gh-123 or GH-123
.replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
// Matches eamodio/vscode-gitlens#123
.replace(
issueEnricher3rdParyRegex,
`[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1")`
)
);
}
// enrichMessage(message: string): string {
// return (
// message
// // Matches #123 or gh-123 or GH-123
// .replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
// // Matches eamodio/vscode-gitlens#123
// .replace(
// issueEnricher3rdParyRegex,
// `[$&](${this.protocol}://${this.domain}/$1/issues/$2 "Open Issue #$2 from $1")`
// )
// );
// }
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;

+ 16
- 10
src/git/remotes/gitlab.ts Просмотреть файл

@ -1,14 +1,28 @@
'use strict';
import { Range } from 'vscode';
import { RemoteProvider } from './provider';
const issueEnricherRegex = /(^|\s)(\\?#([0-9]+))\b/gi;
import { AutolinkReference } from '../../config';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
export class GitLabRemote extends RemoteProvider {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
if (this._autolinks === undefined) {
this._autolinks = [
{
prefix: '#',
url: `${this.baseUrl}/issues/<num>`,
title: 'Open Issue #<num>'
}
];
}
return this._autolinks;
}
get icon() {
return 'gitlab';
}
@ -17,14 +31,6 @@ export class GitLabRemote extends RemoteProvider {
return this.formatName('GitLab');
}
enrichMessage(message: string): string {
return (
message
// Matches #123
.replace(issueEnricherRegex, `$1[$2](${this.baseUrl}/issues/$3 "Open Issue $2")`)
);
}
protected getUrlForBranches(): string {
return `${this.baseUrl}/branches`;
}

+ 6
- 4
src/git/remotes/provider.ts Просмотреть файл

@ -1,8 +1,10 @@
'use strict';
import { env, Range, Uri, window } from 'vscode';
import { AutolinkReference } from '../../config';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { GitLogCommit } from '../models/logCommit';
import { DynamicAutolinkReference } from '../../annotations/autolinks';
export enum RemoteResourceType {
Branch = 'branch',
@ -75,6 +77,10 @@ export abstract class RemoteProvider {
this._name = name;
}
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
return [];
}
get icon(): string {
return 'remote';
}
@ -89,10 +95,6 @@ export abstract class RemoteProvider {
return `${this.protocol}://${this.domain}/${this.path}`;
}
enrichMessage(message: string): string {
return message;
}
protected formatName(name: string) {
if (this._name !== undefined) return this._name;
return `${name}${this.custom ? ` (${this.domain})` : ''}`;

Загрузка…
Отмена
Сохранить