Skip to content

Commit 9e97fab

Browse files
andrewsavage1eamodio
authored andcommitted
Fixes #720 - adds Gerrit remote provider
1 parent 4a0b4a6 commit 9e97fab

File tree

7 files changed

+187
-8
lines changed

7 files changed

+187
-8
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Large diffs are not rendered by default.

images/dark/icon-gerrit.svg

Lines changed: 9 additions & 0 deletions
Loading

images/light/icon-gerrit.svg

Lines changed: 9 additions & 0 deletions
Loading

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,7 @@
20972097
"AzureDevOps",
20982098
"Bitbucket",
20992099
"BitbucketServer",
2100+
"Gerrit",
21002101
"Gitea",
21012102
"GitHub",
21022103
"GitLab"

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export enum CustomRemoteType {
221221
Bitbucket = 'Bitbucket',
222222
BitbucketServer = 'BitbucketServer',
223223
Custom = 'Custom',
224+
Gerrit = 'Gerrit',
224225
Gitea = 'Gitea',
225226
GitHub = 'GitHub',
226227
GitLab = 'GitLab',

src/git/remotes/factory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AzureDevOpsRemote } from './azure-devops';
55
import { BitbucketRemote } from './bitbucket';
66
import { BitbucketServerRemote } from './bitbucket-server';
77
import { CustomRemote } from './custom';
8+
import { GerritRemote } from './gerrit';
89
import { GiteaRemote } from './gitea';
910
import { GitHubRemote } from './github';
1011
import { GitLabRemote } from './gitlab';
@@ -58,6 +59,11 @@ const builtInProviders: RemoteProviders = [
5859
matcher: /\bgitea\b/i,
5960
creator: (domain: string, path: string) => new GiteaRemote(domain, path),
6061
},
62+
{
63+
custom: false,
64+
matcher: /\bgooglesource\.com$/i,
65+
creator: (domain: string, path: string) => new GerritRemote(domain, path),
66+
},
6167
];
6268

6369
export class RemoteProviderFactory {
@@ -135,6 +141,8 @@ export class RemoteProviderFactory {
135141
case CustomRemoteType.Custom:
136142
return (domain: string, path: string) =>
137143
new CustomRemote(domain, path, cfg.urls!, cfg.protocol, cfg.name);
144+
case CustomRemoteType.Gerrit:
145+
return (domain: string, path: string) => new GerritRemote(domain, path, cfg.protocol, cfg.name, true);
138146
case CustomRemoteType.Gitea:
139147
return (domain: string, path: string) => new GiteaRemote(domain, path, cfg.protocol, cfg.name, true);
140148
case CustomRemoteType.GitHub:

src/git/remotes/gerrit.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'use strict';
2+
import { Range, Uri } from 'vscode';
3+
import { DynamicAutolinkReference } from '../../annotations/autolinks';
4+
import { AutolinkReference } from '../../config';
5+
import { GitRevision } from '../models/models';
6+
import { Repository } from '../models/repository';
7+
import { RemoteProvider } from './factory';
8+
9+
const fileRegex = /^\/([^/]+)\/\+(.+)$/i;
10+
const rangeRegex = /^(\d+)$/;
11+
12+
export class GerritRemote extends RemoteProvider {
13+
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
14+
super(domain, path, protocol, name, custom);
15+
}
16+
17+
private _autolinks: (AutolinkReference | DynamicAutolinkReference)[] | undefined;
18+
override get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
19+
if (this._autolinks === undefined) {
20+
this._autolinks = [
21+
{
22+
prefix: 'Change-Id: ',
23+
url: `${this.baseReviewUrl}/q/<num>`,
24+
title: `Open Change #<num> on ${this.name}`,
25+
alphanumeric: true,
26+
},
27+
];
28+
}
29+
return this._autolinks;
30+
}
31+
32+
override get icon() {
33+
return 'gerrit';
34+
}
35+
36+
get id() {
37+
return 'gerrit';
38+
}
39+
40+
get name() {
41+
return this.formatName('Gerrit');
42+
}
43+
44+
private get reviewDomain(): string {
45+
const [subdomain, secondLevelDomain, topLevelDomain] = this.domain.split('.');
46+
return [`${subdomain}-review`, secondLevelDomain, topLevelDomain].join('.');
47+
}
48+
49+
private get baseReviewUrl(): string {
50+
return `${this.protocol}://${this.reviewDomain}`;
51+
}
52+
53+
async getLocalInfoFromRemoteUri(
54+
repository: Repository,
55+
uri: Uri,
56+
options?: { validate?: boolean },
57+
): Promise<{ uri: Uri; startLine?: number } | undefined> {
58+
if (uri.authority !== this.domain) return undefined;
59+
if ((options?.validate ?? true) && !uri.path.startsWith(`/${this.path}/`)) return undefined;
60+
61+
let startLine;
62+
if (uri.fragment) {
63+
const match = rangeRegex.exec(uri.fragment);
64+
if (match != null) {
65+
const [, start] = match;
66+
if (start) {
67+
startLine = parseInt(start, 10);
68+
}
69+
}
70+
}
71+
72+
const match = fileRegex.exec(uri.path);
73+
if (match == null) return undefined;
74+
75+
const [, , path] = match;
76+
77+
// Check for a permalink
78+
let index = path.indexOf('/', 1);
79+
if (index !== -1) {
80+
const sha = path.substring(1, index);
81+
if (GitRevision.isSha(sha) || sha == 'HEAD') {
82+
const uri = repository.toAbsoluteUri(path.substr(index), { validate: options?.validate });
83+
if (uri != null) return { uri: uri, startLine: startLine };
84+
}
85+
}
86+
87+
// Check for a link with branch (and deal with branch names with /)
88+
if (path.startsWith('/refs/heads/')) {
89+
const branches = new Set<string>(
90+
(
91+
await repository.getBranches({
92+
filter: b => b.remote,
93+
})
94+
).map(b => b.getNameWithoutRemote()),
95+
);
96+
const branchPath = path.substr('/refs/heads/'.length);
97+
98+
do {
99+
index = branchPath.lastIndexOf('/', index - 1);
100+
const branch = branchPath.substring(0, index);
101+
102+
if (branches.has(branch)) {
103+
const uri = repository.toAbsoluteUri(branchPath.substr(index), { validate: options?.validate });
104+
if (uri != null) return { uri: uri, startLine: startLine };
105+
}
106+
} while (index > 0);
107+
108+
return undefined;
109+
}
110+
111+
// Check for a link with tag (and deal with tag names with /)
112+
if (path.startsWith('/refs/tags/')) {
113+
const tags = new Set<string>((await repository.getTags()).map(t => t.name));
114+
const tagPath = path.substr('/refs/tags/'.length);
115+
116+
do {
117+
index = tagPath.lastIndexOf('/', index - 1);
118+
const tag = tagPath.substring(0, index);
119+
120+
if (tags.has(tag)) {
121+
const uri = repository.toAbsoluteUri(tagPath.substr(index), { validate: options?.validate });
122+
if (uri != null) return { uri: uri, startLine: startLine };
123+
}
124+
} while (index > 0);
125+
126+
return undefined;
127+
}
128+
129+
return undefined;
130+
}
131+
132+
protected getUrlForBranches(): string {
133+
return this.encodeUrl(`${this.baseReviewUrl}/admin/repos/${this.path},branches`);
134+
}
135+
136+
protected getUrlForBranch(branch: string): string {
137+
return this.encodeUrl(`${this.baseUrl}/+/refs/heads/${branch}`);
138+
}
139+
140+
protected getUrlForCommit(sha: string): string {
141+
return this.encodeUrl(`${this.baseReviewUrl}/q/${sha}`);
142+
}
143+
144+
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
145+
const line = range != null ? `#${range.start.line}` : '';
146+
147+
if (sha) return `${this.encodeUrl(`${this.baseUrl}/+/${sha}/${fileName}`)}${line}`;
148+
if (branch) return `${this.encodeUrl(`${this.getUrlForBranch(branch)}/${fileName}`)}${line}`;
149+
return `${this.encodeUrl(`${this.baseUrl}/+/HEAD/${fileName}`)}${line}`;
150+
}
151+
}

0 commit comments

Comments
 (0)