Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.

Commit 4a76e34

Browse files
authored
feat: Gitlab (#197)
Add support for GitLab. It implements GitLab as a `CodeHost` so that it is injected via the new inject method.
1 parent 71791ff commit 4a76e34

File tree

12 files changed

+719
-17
lines changed

12 files changed

+719
-17
lines changed

src/extension/manifest.spec.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
{
4444
"matches": [
4545
"https://github.com/*",
46+
"https://gitlab.com/*",
4647
"https://sourcegraph.com/*",
4748
"https://localhost:3443/*",
4849
"http://localhost:32773/*"
@@ -57,6 +58,7 @@
5758
"activeTab",
5859
"contextMenus",
5960
"https://github.com/*",
61+
"https://gitlab.com/*",
6062
"https://localhost:3443/*",
6163
"https://sourcegraph.com/*",
6264
"http://localhost:32773/*"
@@ -66,7 +68,7 @@
6668
"prod": {
6769
"content_scripts": [
6870
{
69-
"matches": ["https://github.com/*", "https://sourcegraph.com/*"],
71+
"matches": ["https://github.com/*", "https://gitlab.com/*", "https://sourcegraph.com/*"],
7072
"run_at": "document_end",
7173
"js": ["js/inject.bundle.js"]
7274
}
@@ -77,6 +79,7 @@
7779
"storage",
7880
"contextMenus",
7981
"https://github.com/*",
82+
"https://gitlab.com/*",
8083
"https://sourcegraph.com/*"
8184
]
8285
}

src/extension/scripts/inject.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { featureFlags } from '../../shared/util/featureFlags'
1919
import { injectBitbucketServer } from '../../libs/bitbucket/inject'
2020
import { injectCodeIntelligence } from '../../libs/code_intelligence'
2121
import { injectGitHubApplication } from '../../libs/github/inject'
22+
import { checkIsGitlab } from '../../libs/gitlab/code_intelligence'
2223
import { injectPhabricatorApplication } from '../../libs/phabricator/app'
2324
import { injectSourcegraphApp } from '../../libs/sourcegraph/inject'
2425
import { assertEnv } from '../envAssertion'
@@ -56,14 +57,15 @@ function injectApplication(): void {
5657
const isBitbucket =
5758
document.querySelector('.bitbucket-header-logo') ||
5859
document.querySelector('.aui-header-logo.aui-header-logo-bitbucket')
60+
const isGitlab = checkIsGitlab()
5961

6062
if (!isSourcegraphServer && !document.getElementById('ext-style-sheet')) {
6163
if (window.safari) {
6264
runtime.sendMessage({
6365
type: 'insertCSS',
6466
payload: { file: 'css/style.bundle.css', origin: window.location.origin },
6567
})
66-
} else if (isPhabricator || isGitHub || isGitHubEnterprise || isBitbucket) {
68+
} else if (isPhabricator || isGitHub || isGitHubEnterprise || isBitbucket || isGitlab) {
6769
const styleSheet = document.createElement('link') as HTMLLinkElement
6870
styleSheet.id = 'ext-style-sheet'
6971
styleSheet.rel = 'stylesheet'
@@ -101,8 +103,8 @@ function injectApplication(): void {
101103
injectBitbucketServer()
102104
}
103105

104-
if (isGitHub || isPhabricator) {
105-
if (await featureFlags.isEnabled('newInject')) {
106+
if (isGitHub || isPhabricator || isGitlab) {
107+
if (isGitlab || (await featureFlags.isEnabled('newInject'))) {
106108
const subscriptions = await injectCodeIntelligence()
107109
window.addEventListener('unload', () => subscriptions.unsubscribe())
108110
}

src/libs/code_intelligence/HoverOverlay.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@
1616
}
1717
}
1818
}
19+
.hover-overlay-mount__gitlab {
20+
.hover-overlay {
21+
z-index: 1000;
22+
}
23+
}

src/libs/code_intelligence/code_intelligence.tsx

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import { lspViaAPIXlang } from '../../shared/backend/lsp'
2323
import { ButtonProps, CodeViewToolbar } from '../../shared/components/CodeViewToolbar'
2424
import { eventLogger, sourcegraphUrl } from '../../shared/util/context'
2525
import { githubCodeHost } from '../github/code_intelligence'
26+
import { gitlabCodeHost } from '../gitlab/code_intelligence'
2627
import { phabricatorCodeHost } from '../phabricator/code_intelligence'
2728
import { findCodeViews } from './code_views'
29+
import { initSearch, SearchFeature } from './search'
2830

2931
/**
3032
* Defines a type of code view a given code host can have. It tells us how to
@@ -64,13 +66,24 @@ export interface CodeViewResolver {
6466
resolveCodeView: (elem: HTMLElement) => CodeViewWithOutSelector
6567
}
6668

69+
interface OverlayPosition {
70+
top: number
71+
left: number
72+
}
73+
6774
/** Information for adding code intelligence to code views on arbitrary code hosts. */
6875
export interface CodeHost {
6976
/**
7077
* The name of the code host. This will be added as a className to the overlay mount.
7178
*/
7279
name: string
7380

81+
/**
82+
* Checks to see if the current context the code is running in is within
83+
* the given code host.
84+
*/
85+
check: () => Promise<boolean> | boolean
86+
7487
/**
7588
* The list of types of code views to try to annotate.
7689
*/
@@ -83,10 +96,16 @@ export interface CodeHost {
8396
codeViewResolver?: CodeViewResolver
8497

8598
/**
86-
* Checks to see if the current context the code is running in is within
87-
* the given code host.
99+
* Adjust the position of the hover overlay. Useful for fixed headers or other
100+
* elements that throw off the position of the tooltip within the relative
101+
* element.
88102
*/
89-
check: () => Promise<boolean> | boolean
103+
adjustOverlayPosition?: (position: OverlayPosition) => OverlayPosition
104+
105+
/**
106+
* Implementation of the search feature for a code host.
107+
*/
108+
search?: SearchFeature
90109
}
91110

92111
export interface FileInfo {
@@ -150,11 +169,19 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
150169
const hoverOverlayElements = new Subject<HTMLElement | null>()
151170
const nextOverlayElement = (element: HTMLElement | null) => hoverOverlayElements.next(element)
152171

153-
const overlayMount = document.createElement('div')
154-
overlayMount.style.height = '0px'
155-
overlayMount.classList.add('hover-overlay-mount')
156-
overlayMount.classList.add(`hover-overlay-mount__${codeHost.name}`)
157-
document.body.appendChild(overlayMount)
172+
const classNames = ['hover-overlay-mount', `hover-overlay-mount__${codeHost.name}`]
173+
174+
const createMount = () => {
175+
const overlayMount = document.createElement('div')
176+
overlayMount.style.height = '0px'
177+
for (const className of classNames) {
178+
overlayMount.classList.add(className)
179+
}
180+
document.body.appendChild(overlayMount)
181+
return overlayMount
182+
}
183+
184+
const overlayMount = document.querySelector(`.${classNames.join('.')}`) || createMount()
158185

159186
const relativeElement = document.body
160187

@@ -201,9 +228,10 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
201228
containerComponentUpdates.next()
202229
}
203230
public render(): JSX.Element | null {
204-
return this.state.hoverOverlayProps ? (
231+
const hoverOverlayProps = this.getHoverOverlayProps()
232+
return hoverOverlayProps ? (
205233
<HoverOverlay
206-
{...this.state.hoverOverlayProps}
234+
{...hoverOverlayProps}
207235
linkComponent={Link}
208236
logTelemetryEvent={this.log}
209237
hoverRef={nextOverlayElement}
@@ -213,6 +241,21 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
213241
) : null
214242
}
215243
private log = () => eventLogger.logCodeIntelligenceEvent()
244+
private getHoverOverlayProps(): HoverState['hoverOverlayProps'] {
245+
if (!this.state.hoverOverlayProps) {
246+
return undefined
247+
}
248+
249+
let { overlayPosition, ...rest } = this.state.hoverOverlayProps
250+
if (overlayPosition && codeHost.adjustOverlayPosition) {
251+
overlayPosition = codeHost.adjustOverlayPosition(overlayPosition)
252+
}
253+
254+
return {
255+
...rest,
256+
overlayPosition,
257+
}
258+
}
216259
}
217260

218261
render(<HoverOverlayContainer />, overlayMount)
@@ -230,6 +273,10 @@ export interface ResolvedCodeView extends CodeViewWithOutSelector {
230273
}
231274

232275
function handleCodeHost(codeHost: CodeHost): Subscription {
276+
if (codeHost.search) {
277+
initSearch(codeHost.search)
278+
}
279+
233280
const { hoverifier } = initCodeIntelligence(codeHost)
234281

235282
const subscriptions = new Subscription()
@@ -247,7 +294,7 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
247294
const resolveContext: ContextResolver = ({ part }) => ({
248295
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
249296
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
250-
filePath: part === 'base' ? info.baseFilePath! : info.filePath,
297+
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
251298
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
252299
})
253300

@@ -308,7 +355,7 @@ async function injectCodeIntelligenceToCodeHosts(codeHosts: CodeHost[]): Promise
308355
* incomplete setup requests.
309356
*/
310357
export async function injectCodeIntelligence(): Promise<Subscription> {
311-
const codeHosts: CodeHost[] = [githubCodeHost, phabricatorCodeHost]
358+
const codeHosts: CodeHost[] = [githubCodeHost, gitlabCodeHost, phabricatorCodeHost]
312359

313360
return await injectCodeIntelligenceToCodeHosts(codeHosts)
314361
}

src/libs/code_intelligence/search.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import storage from '../../browser/storage'
2+
import { resolveRev } from '../../shared/repo/backend'
3+
import { getPlatformName, repoUrlCache, sourcegraphUrl } from '../../shared/util/context'
4+
5+
export interface SearchPageInformation {
6+
query: string
7+
repoPath: string
8+
rev?: string
9+
}
10+
11+
/**
12+
* Interface containing information needed for the search feature.
13+
*/
14+
export interface SearchFeature {
15+
/**
16+
* Check that we're on the search page.
17+
*/
18+
checkIsSearchPage: () => boolean
19+
/**
20+
* Get information required for executing a search.
21+
*/
22+
getRepoInformation: () => SearchPageInformation
23+
}
24+
25+
function getSourcegraphURLProps({
26+
repoPath,
27+
rev,
28+
query,
29+
}: SearchPageInformation): { url: string; repo: string; rev: string | undefined; query: string } | undefined {
30+
if (repoPath) {
31+
if (rev) {
32+
return {
33+
url: `search?q=${encodeURIComponent(query)}&sq=repo:%5E${encodeURIComponent(
34+
repoPath.replace(/\./g, '\\.')
35+
)}%24@${encodeURIComponent(rev)}&utm_source=${getPlatformName()}`,
36+
repo: repoPath,
37+
rev,
38+
query: `${encodeURIComponent(query)} ${encodeURIComponent(
39+
repoPath.replace(/\./g, '\\.')
40+
)}%24@${encodeURIComponent(rev)}`,
41+
}
42+
}
43+
44+
return {
45+
url: `search?q=${encodeURIComponent(query)}&sq=repo:%5E${encodeURIComponent(
46+
repoPath.replace(/\./g, '\\.')
47+
)}%24&utm_source=${getPlatformName()}`,
48+
repo: repoPath,
49+
rev,
50+
query: `repo:^${repoPath.replace(/\./g, '\\.')}$ ${query}`,
51+
}
52+
}
53+
}
54+
55+
export function initSearch({ getRepoInformation, checkIsSearchPage }: SearchFeature): void {
56+
if (checkIsSearchPage()) {
57+
storage.getSync(({ executeSearchEnabled }) => {
58+
// GitHub search page pathname is <org>/<repo>/search
59+
if (!executeSearchEnabled) {
60+
return
61+
}
62+
63+
const { repoPath, rev, query } = getRepoInformation()
64+
if (query) {
65+
const linkProps = getSourcegraphURLProps({ repoPath, rev, query })
66+
67+
if (linkProps) {
68+
// Ensure that we open the correct sourcegraph server url by checking which
69+
// server instance can access the repository.
70+
resolveRev({ repoPath: linkProps.repo }).subscribe(() => {
71+
const baseUrl = repoUrlCache[linkProps.repo] || sourcegraphUrl
72+
const url = `${baseUrl}/${linkProps.url}`
73+
window.open(url, '_blank')
74+
})
75+
}
76+
}
77+
})
78+
}
79+
}

src/libs/gitlab/api.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { first } from 'lodash'
2+
import { Observable } from 'rxjs'
3+
import { ajax } from 'rxjs/ajax'
4+
import { map } from 'rxjs/operators'
5+
6+
import { memoizeObservable } from '../../shared/util/memoize'
7+
import { GitLabDiffInfo } from './scrape'
8+
9+
/**
10+
* Significant revisions for a merge request.
11+
*/
12+
interface DiffRefs {
13+
base_sha: string
14+
head_sha: string
15+
start_sha: string
16+
}
17+
18+
/**
19+
* Response from the GitLab API for fetching a merge request. Note that there
20+
* is more information returned but we are not using it.
21+
*/
22+
interface MergeRequestResponse {
23+
diff_refs: DiffRefs
24+
}
25+
26+
/**
27+
* Response from the GitLab API for fetching a specific version(diff) of a merge
28+
* request. Note that there is more information returned but we are not using it.
29+
*/
30+
interface DiffVersionsResponse {
31+
base_commit_sha: string
32+
}
33+
34+
type GetBaseCommitIDInput = Pick<GitLabDiffInfo, 'owner' | 'repoName' | 'mergeRequestID' | 'diffID'>
35+
36+
const buildURL = (owner: string, repoName: string, path: string) =>
37+
`${window.location.origin}/api/v4/projects/${owner}%2f${repoName}${path}`
38+
39+
const get = <T>(url: string): Observable<T> => ajax.get(url).pipe(map(({ response }) => response as T))
40+
41+
/**
42+
* Get the base commit ID for a merge request.
43+
*/
44+
export const getBaseCommitIDForMergeRequest: (info: GetBaseCommitIDInput) => Observable<string> = memoizeObservable(
45+
({ owner, repoName, mergeRequestID, diffID }: GetBaseCommitIDInput) => {
46+
const mrURL = buildURL(owner, repoName, `/merge_requests/${mergeRequestID}`)
47+
48+
// If we have a `diffID`, retrieve the information for that individual diff.
49+
if (diffID) {
50+
return get<DiffVersionsResponse>(`${mrURL}/versions/${diffID}`).pipe(
51+
map(({ base_commit_sha }) => base_commit_sha)
52+
)
53+
}
54+
55+
// Otherwise, just get the overall base `commitID` for the merge request.
56+
return get<MergeRequestResponse>(mrURL).pipe(map(({ diff_refs: { base_sha } }) => base_sha))
57+
},
58+
({ mergeRequestID, diffID }) => mergeRequestID + (diffID ? `/${diffID}` : '')
59+
)
60+
61+
interface CommitResponse {
62+
parent_ids: string[]
63+
}
64+
65+
/**
66+
* Get the base commit ID for a commit.
67+
*/
68+
export const getBaseCommitIDForCommit: (
69+
{ owner, repoName, commitID }: Pick<GetBaseCommitIDInput, 'owner' | 'repoName'> & { commitID: string }
70+
) => Observable<string> = memoizeObservable(({ owner, repoName, commitID }) =>
71+
get<CommitResponse>(buildURL(owner, repoName, `/repository/commits/${commitID}`)).pipe(
72+
map(({ parent_ids }) => first(parent_ids)!) // ! because it'll always have a parent if we are looking at the commit page.
73+
)
74+
)

0 commit comments

Comments
 (0)