Skip to content

Commit 9c1082c

Browse files
committed
Handled hashes in file names for GitHub URLs.
Fixes #34
1 parent 1384c1e commit 9c1082c

File tree

10 files changed

+90
-12
lines changed

10 files changed

+90
-12
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# 2.5.2 (2021-08-09)
2+
3+
## Bug Fixes
4+
5+
- 🐛 GitHub links now handle hashes in file names.
6+
17
# 2.5.1 (2021-07-10)
28

3-
## Changes
9+
## Bug Fixes
410

511
- 🐛 GitHub links now prevent markdown files from being rendered.
612
- 🐛 Selections that end at the start of a new line are adjusted to end at the end of the previous line.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-gitweblinks",
33
"displayName": "Git Web Links for VS Code",
44
"description": "Copy links to files in their online Git repositories",
5-
"version": "2.5.1",
5+
"version": "2.5.2",
66
"publisher": "reduckted",
77
"homepage": "https://github.com/reduckted/vscode-gitweblinks",
88
"repository": {

shared/handler-schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,6 @@
441441
}
442442
},
443443
"oneOf": [{ "required": ["server"] }, { "required": ["private"] }],
444-
"required": ["name", "branch", "url", "selection", "reverse", "tests"],
444+
"required": ["name", "branchRef", "url", "selection", "reverse", "tests"],
445445
"additionalProperties": false
446446
}

shared/handlers/github-enterprise.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "GitHub Enterprise",
44
"private": "gitHubEnterprise",
55
"branchRef": "abbreviated",
6-
"url": "{{ base }}/{{ repository }}/blob/{{ ref | encode_uri }}/{{ file | encode_uri }}?plain=1",
6+
"url": "{{ base }}/{{ repository }}/blob/{{ ref | encode_uri }}/{{ file | encode_uri_component_segments }}?plain=1",
77
"selection": "#L{{ startLine }}{% if startLine != endLine %}-L{{ endLine }}{% endif %}",
88
"reverse": {
99
"pattern": [
@@ -15,7 +15,7 @@
1515
"(?:\\?[^#]*)?",
1616
"(?:#L(?<start>\\d+)(?:-L(?<end>\\d+))?)?"
1717
],
18-
"file": "{{ match.groups.file | decode_uri }}",
18+
"file": "{{ match.groups.file | decode_uri_component_segments }}",
1919
"fileMayStartWithBranch": true,
2020
"server": {
2121
"http": "{{ http }}/{{ match.groups.username }}/{{ match.groups.repository }}.git",
@@ -55,6 +55,14 @@
5555
"remote": "https://local-github.server:8080/context/foo/bar.git",
5656
"result": "https://local-github.server:8080/context/foo/bar/blob/{{ commit }}/src/file.txt?plain=1"
5757
},
58+
"misc": [
59+
{
60+
"name": "Hash in file name",
61+
"fileName": "test#file",
62+
"remote": "https://local-github.server:8080/context/foo/bar.git",
63+
"result": "https://local-github.server:8080/context/foo/bar/blob/master/test%23file?plain=1"
64+
}
65+
],
5866
"selection": {
5967
"remote": "https://local-github.server:8080/context/foo/bar.git",
6068
"point": {

shared/handlers/github.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
},
88
"branchRef": "abbreviated",
9-
"url": "{{ base }}/{{ repository }}/blob/{{ ref | encode_uri }}/{{ file | encode_uri }}?plain=1",
9+
"url": "{{ base }}/{{ repository }}/blob/{{ ref | encode_uri }}/{{ file | encode_uri_component_segments }}?plain=1",
1010
"selection": "#L{{ startLine }}{% if startLine != endLine %}-L{{ endLine }}{% endif %}",
1111
"reverse": {
1212
"pattern": [
@@ -19,7 +19,7 @@
1919
"(?:\\?[^#]*)?",
2020
"(?:#L(?<start>\\d+)(?:-L(?<end>\\d+))?)?"
2121
],
22-
"file": "{{ match.groups.file | decode_uri }}",
22+
"file": "{{ match.groups.file | decode_uri_component_segments }}",
2323
"fileMayStartWithBranch": true,
2424
"server": {
2525
"http": "https://github.com/{{ match.groups.username }}/{{ match.groups.repository }}.git",
@@ -51,6 +51,14 @@
5151
"remote": "https://github.com/foo/bar.git",
5252
"result": "https://github.com/foo/bar/blob/{{ commit }}/src/file.txt?plain=1"
5353
},
54+
"misc": [
55+
{
56+
"name": "Hash in file name",
57+
"fileName": "test#file",
58+
"remote": "https://github.com/foo/bar.git",
59+
"result": "https://github.com/foo/bar/blob/master/test%23file?plain=1"
60+
}
61+
],
5462
"selection": {
5563
"remote": "https://github.com/foo/bar.git",
5664
"point": {

src/commands/get-link-command.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class GetLinkCommand {
9595
break;
9696

9797
case 'open':
98-
void env.openExternal(Uri.parse(link));
98+
openExternal(link);
9999
}
100100
} catch (ex) {
101101
log('Error while generating a link: %o', ex);
@@ -171,13 +171,30 @@ export class GetLinkCommand {
171171

172172
case 'open':
173173
if (link) {
174-
void env.openExternal(Uri.parse(link));
174+
openExternal(link);
175175
}
176176
break;
177177
}
178178
}
179179
}
180180

181+
/**
182+
* A wrapper around `env.openExternal()` to handle a bug in VS Code.
183+
*
184+
* @param link The link to open.
185+
*/
186+
function openExternal(link: string): void {
187+
try {
188+
// @ts-expect-error: VS Code seems to decode and re-encode the URI, which causes certain
189+
// characters to be unescaped and breaks the URL. A a hack, we can try passing a string
190+
// instead of a URI. If that throws an error, then we'll fall back to passing a URI.
191+
// https://github.com/microsoft/vscode/issues/85930
192+
void env.openExternal(link);
193+
} catch {
194+
void env.openExternal(Uri.parse(link));
195+
}
196+
}
197+
181198
/**
182199
* Options for controling the behaviouor of the command.
183200
*/

src/templates.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ const engine: Liquid = new Liquid({
1010
engine.filters.set('filename', posix.basename);
1111
engine.filters.set('encode_uri', encodeURI);
1212
engine.filters.set('encode_uri_component', encodeURIComponent);
13+
engine.filters.set('encode_uri_component_segments', encodeURIComponentSegments);
1314
engine.filters.set('decode_uri', decodeURI);
1415
engine.filters.set('decode_uri_component', decodeURIComponent);
16+
engine.filters.set('decode_uri_component_segments', decodeURIComponentSegments);
1517

1618
/**
1719
* Parses the given template.
@@ -57,6 +59,29 @@ export function parseTemplate(template: Template | undefined): ParsedTemplate |
5759
};
5860
}
5961

62+
/**
63+
* A template filter that splits the value into path segments (at the '/' character)
64+
* and applies `encodeURIComponent` on each segment before joining the segments back together.
65+
*
66+
* @param value The value to transform.
67+
* @returns The transformed value.
68+
*/
69+
function encodeURIComponentSegments(value: string): string {
70+
// Split the value into path segments so that
71+
// `encodeURIComponent` doesn't encode the '/' character.
72+
return value.split('/').map(encodeURIComponent).join('/');
73+
}
74+
75+
/**
76+
* A template filter that reverses `encodeURIComponentSegments`.
77+
*
78+
* @param value The value to transform.
79+
* @returns The transformed value.
80+
*/
81+
function decodeURIComponentSegments(value: string): string {
82+
return value.split('/').map(decodeURIComponent).join('/');
83+
}
84+
6085
/**
6186
* Represents a template that has been pared and can be rendered.
6287
*/

test/commands/get-link-command.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ describe('GetLinkCommand', () => {
233233
command = createCommand({ linkType: 'commit', includeSelection: true, action: 'copy' });
234234
await command.execute(file);
235235

236-
expect(openExternal).to.have.been.calledWith(Uri.parse('http://example.com/foo/bar'));
236+
// Should be called with a string instead of a Uri because of https://github.com/microsoft/vscode/issues/85930
237+
expect(openExternal).to.have.been.calledWith('http://example.com/foo/bar');
237238
});
238239

239240
it('should open the link in the browser without showing a notification when the command action is "open".', async () => {
@@ -248,7 +249,8 @@ describe('GetLinkCommand', () => {
248249
await command.execute(file);
249250

250251
expect(showInformationMessage).to.have.not.been.called;
251-
expect(openExternal).to.have.been.calledWith(Uri.parse('http://example.com/foo/bar'));
252+
// Should be called with a string instead of a Uri because of https://github.com/microsoft/vscode/issues/85930
253+
expect(openExternal).to.have.been.calledWith('http://example.com/foo/bar');
252254
});
253255

254256
function createCommand(options: GetLinkCommandOptions): GetLinkCommand {

test/templates.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ describe('templates', () => {
3939
expect(template.render({})).to.equal('This%20%2B%20that');
4040
});
4141

42+
it('should support encodeUriComponent() on path segments.', () => {
43+
template = parseTemplate('This{{ "/a/b#c/d/" | encode_uri_component_segments }}that');
44+
45+
expect(template.render({})).to.equal('This/a/b%23c/d/that');
46+
});
47+
4248
it('should support decodeUri().', () => {
4349
template = parseTemplate('This{{ "%20+%20" | decode_uri }}that');
4450

@@ -51,6 +57,12 @@ describe('templates', () => {
5157
expect(template.render({})).to.equal('This + that');
5258
});
5359

60+
it('should support decodeUriComponent() on path segments.', () => {
61+
template = parseTemplate('This{{ "/a/b%23c/d/" | decode_uri_component_segments }}that');
62+
63+
expect(template.render({})).to.equal('This/a/b#c/d/that');
64+
});
65+
5466
it('should support path.basename().', () => {
5567
template = parseTemplate('The name is {{ "foo/bar/meep.ts" | filename }}.');
5668

0 commit comments

Comments
 (0)