Skip to content

Commit

Permalink
Closes #897 - Adds autolink support
Browse files Browse the repository at this point in the history
  • Loading branch information
eamodio committed Nov 18, 2019
1 parent 2ac718b commit 59e7685
Show file tree
Hide file tree
Showing 14 changed files with 283 additions and 76 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/annotations/annotations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use strict';
import {
DecorationInstanceRenderOptions,
DecorationOptions,
Expand Down
84 changes: 84 additions & 0 deletions src/annotations/autolinks.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { TraceLevel } from './logger';

export interface Config {
autolinks: AutolinkReference[] | null;
blame: {
avatars: boolean;
compact: boolean;
Expand Down Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 11 additions & 10 deletions src/git/formatters/commitFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,7 @@ export class CommitFormatter extends Formatter<GitCommit, CommitFormatOptions> {
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}`;
}
Expand Down Expand Up @@ -357,3 +348,13 @@ export class CommitFormatter extends Formatter<GitCommit, CommitFormatOptions> {
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);
30 changes: 18 additions & 12 deletions src/git/remotes/azure-devops.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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';
}
Expand All @@ -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`;
}
Expand Down
34 changes: 21 additions & 13 deletions src/git/remotes/bitbucket-server.ts
Original file line number Diff line number Diff line change
@@ -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('/')
Expand All @@ -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`;
}
Expand Down
Loading

0 comments on commit 59e7685

Please sign in to comment.