Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.
5 changes: 4 additions & 1 deletion src/extension/manifest.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
{
"matches": [
"https://github.com/*",
"https://gitlab.com/*",
"https://sourcegraph.com/*",
"https://localhost:3443/*",
"http://localhost:32773/*"
Expand All @@ -57,6 +58,7 @@
"activeTab",
"contextMenus",
"https://github.com/*",
"https://gitlab.com/*",
"https://localhost:3443/*",
"https://sourcegraph.com/*",
"http://localhost:32773/*"
Expand All @@ -66,7 +68,7 @@
"prod": {
"content_scripts": [
{
"matches": ["https://github.com/*", "https://sourcegraph.com/*"],
"matches": ["https://github.com/*", "https://gitlab.com/*", "https://sourcegraph.com/*"],
"run_at": "document_end",
"js": ["js/inject.bundle.js"]
}
Expand All @@ -77,6 +79,7 @@
"storage",
"contextMenus",
"https://github.com/*",
"https://gitlab.com/*",
"https://sourcegraph.com/*"
]
}
Expand Down
8 changes: 5 additions & 3 deletions src/extension/scripts/inject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { featureFlags } from '../../shared/util/featureFlags'
import { injectBitbucketServer } from '../../libs/bitbucket/inject'
import { injectCodeIntelligence } from '../../libs/code_intelligence'
import { injectGitHubApplication } from '../../libs/github/inject'
import { checkIsGitlab } from '../../libs/gitlab/code_intelligence'
import { injectPhabricatorApplication } from '../../libs/phabricator/app'
import { injectSourcegraphApp } from '../../libs/sourcegraph/inject'
import { assertEnv } from '../envAssertion'
Expand Down Expand Up @@ -56,14 +57,15 @@ function injectApplication(): void {
const isBitbucket =
document.querySelector('.bitbucket-header-logo') ||
document.querySelector('.aui-header-logo.aui-header-logo-bitbucket')
const isGitlab = checkIsGitlab()

if (!isSourcegraphServer && !document.getElementById('ext-style-sheet')) {
if (window.safari) {
runtime.sendMessage({
type: 'insertCSS',
payload: { file: 'css/style.bundle.css', origin: window.location.origin },
})
} else if (isPhabricator || isGitHub || isGitHubEnterprise || isBitbucket) {
} else if (isPhabricator || isGitHub || isGitHubEnterprise || isBitbucket || isGitlab) {
const styleSheet = document.createElement('link') as HTMLLinkElement
styleSheet.id = 'ext-style-sheet'
styleSheet.rel = 'stylesheet'
Expand Down Expand Up @@ -101,8 +103,8 @@ function injectApplication(): void {
injectBitbucketServer()
}

if (isGitHub || isPhabricator) {
if (await featureFlags.isEnabled('newInject')) {
if (isGitHub || isPhabricator || isGitlab) {
if (isGitlab || (await featureFlags.isEnabled('newInject'))) {
const subscriptions = await injectCodeIntelligence()
window.addEventListener('unload', () => subscriptions.unsubscribe())
}
Expand Down
5 changes: 5 additions & 0 deletions src/libs/code_intelligence/HoverOverlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@
}
}
}
.hover-overlay-mount__gitlab {
.hover-overlay {
z-index: 1000;
}
}
71 changes: 59 additions & 12 deletions src/libs/code_intelligence/code_intelligence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import { lspViaAPIXlang } from '../../shared/backend/lsp'
import { ButtonProps, CodeViewToolbar } from '../../shared/components/CodeViewToolbar'
import { eventLogger, sourcegraphUrl } from '../../shared/util/context'
import { githubCodeHost } from '../github/code_intelligence'
import { gitlabCodeHost } from '../gitlab/code_intelligence'
import { phabricatorCodeHost } from '../phabricator/code_intelligence'
import { findCodeViews } from './code_views'
import { initSearch, SearchFeature } from './search'

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

interface OverlayPosition {
top: number
left: number
}

/** Information for adding code intelligence to code views on arbitrary code hosts. */
export interface CodeHost {
/**
* The name of the code host. This will be added as a className to the overlay mount.
*/
name: string

/**
* Checks to see if the current context the code is running in is within
* the given code host.
*/
check: () => Promise<boolean> | boolean

/**
* The list of types of code views to try to annotate.
*/
Expand All @@ -83,10 +96,16 @@ export interface CodeHost {
codeViewResolver?: CodeViewResolver

/**
* Checks to see if the current context the code is running in is within
* the given code host.
* Adjust the position of the hover overlay. Useful for fixed headers or other
* elements that throw off the position of the tooltip within the relative
* element.
*/
check: () => Promise<boolean> | boolean
adjustOverlayPosition?: (position: OverlayPosition) => OverlayPosition

/**
* Implementation of the search feature for a code host.
*/
search?: SearchFeature
}

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

const overlayMount = document.createElement('div')
overlayMount.style.height = '0px'
overlayMount.classList.add('hover-overlay-mount')
overlayMount.classList.add(`hover-overlay-mount__${codeHost.name}`)
document.body.appendChild(overlayMount)
const classNames = ['hover-overlay-mount', `hover-overlay-mount__${codeHost.name}`]

const createMount = () => {
const overlayMount = document.createElement('div')
overlayMount.style.height = '0px'
for (const className of classNames) {
overlayMount.classList.add(className)
}
document.body.appendChild(overlayMount)
return overlayMount
}

const overlayMount = document.querySelector(`.${classNames.join('.')}`) || createMount()

const relativeElement = document.body

Expand Down Expand Up @@ -201,9 +228,10 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
containerComponentUpdates.next()
}
public render(): JSX.Element | null {
return this.state.hoverOverlayProps ? (
const hoverOverlayProps = this.getHoverOverlayProps()
return hoverOverlayProps ? (
<HoverOverlay
{...this.state.hoverOverlayProps}
{...hoverOverlayProps}
linkComponent={Link}
logTelemetryEvent={this.log}
hoverRef={nextOverlayElement}
Expand All @@ -213,6 +241,21 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
) : null
}
private log = () => eventLogger.logCodeIntelligenceEvent()
private getHoverOverlayProps(): HoverState['hoverOverlayProps'] {
if (!this.state.hoverOverlayProps) {
return undefined
}

let { overlayPosition, ...rest } = this.state.hoverOverlayProps
if (overlayPosition && codeHost.adjustOverlayPosition) {
overlayPosition = codeHost.adjustOverlayPosition(overlayPosition)
}

return {
...rest,
overlayPosition,
}
}
}

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

function handleCodeHost(codeHost: CodeHost): Subscription {
if (codeHost.search) {
initSearch(codeHost.search)
}

const { hoverifier } = initCodeIntelligence(codeHost)

const subscriptions = new Subscription()
Expand All @@ -247,7 +294,7 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
const resolveContext: ContextResolver = ({ part }) => ({
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
filePath: part === 'base' ? info.baseFilePath! : info.filePath,
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
})

Expand Down Expand Up @@ -308,7 +355,7 @@ async function injectCodeIntelligenceToCodeHosts(codeHosts: CodeHost[]): Promise
* incomplete setup requests.
*/
export async function injectCodeIntelligence(): Promise<Subscription> {
const codeHosts: CodeHost[] = [githubCodeHost, phabricatorCodeHost]
const codeHosts: CodeHost[] = [githubCodeHost, gitlabCodeHost, phabricatorCodeHost]

return await injectCodeIntelligenceToCodeHosts(codeHosts)
}
79 changes: 79 additions & 0 deletions src/libs/code_intelligence/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import storage from '../../browser/storage'
import { resolveRev } from '../../shared/repo/backend'
import { getPlatformName, repoUrlCache, sourcegraphUrl } from '../../shared/util/context'

export interface SearchPageInformation {
query: string
repoPath: string
rev?: string
}

/**
* Interface containing information needed for the search feature.
*/
export interface SearchFeature {
/**
* Check that we're on the search page.
*/
checkIsSearchPage: () => boolean
/**
* Get information required for executing a search.
*/
getRepoInformation: () => SearchPageInformation
}

function getSourcegraphURLProps({
repoPath,
rev,
query,
}: SearchPageInformation): { url: string; repo: string; rev: string | undefined; query: string } | undefined {
if (repoPath) {
if (rev) {
return {
url: `search?q=${encodeURIComponent(query)}&sq=repo:%5E${encodeURIComponent(
repoPath.replace(/\./g, '\\.')
)}%24@${encodeURIComponent(rev)}&utm_source=${getPlatformName()}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use the URL API to avoid manual encodeURIComponent calls

repo: repoPath,
rev,
query: `${encodeURIComponent(query)} ${encodeURIComponent(
repoPath.replace(/\./g, '\\.')
)}%24@${encodeURIComponent(rev)}`,
}
}

return {
url: `search?q=${encodeURIComponent(query)}&sq=repo:%5E${encodeURIComponent(
repoPath.replace(/\./g, '\\.')
)}%24&utm_source=${getPlatformName()}`,
repo: repoPath,
rev,
query: `repo:^${repoPath.replace(/\./g, '\\.')}$ ${query}`,
}
}
}

export function initSearch({ getRepoInformation, checkIsSearchPage }: SearchFeature): void {
if (checkIsSearchPage()) {
storage.getSync(({ executeSearchEnabled }) => {
// GitHub search page pathname is <org>/<repo>/search
if (!executeSearchEnabled) {
return
}

const { repoPath, rev, query } = getRepoInformation()
if (query) {
const linkProps = getSourcegraphURLProps({ repoPath, rev, query })

if (linkProps) {
// Ensure that we open the correct sourcegraph server url by checking which
// server instance can access the repository.
resolveRev({ repoPath: linkProps.repo }).subscribe(() => {
const baseUrl = repoUrlCache[linkProps.repo] || sourcegraphUrl
const url = `${baseUrl}/${linkProps.url}`
window.open(url, '_blank')
})
}
}
})
}
}
74 changes: 74 additions & 0 deletions src/libs/gitlab/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { first } from 'lodash'
import { Observable } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { map } from 'rxjs/operators'

import { memoizeObservable } from '../../shared/util/memoize'
import { GitLabDiffInfo } from './scrape'

/**
* Significant revisions for a merge request.
*/
interface DiffRefs {
base_sha: string
head_sha: string
start_sha: string
}

/**
* Response from the GitLab API for fetching a merge request. Note that there
* is more information returned but we are not using it.
*/
interface MergeRequestResponse {
diff_refs: DiffRefs
}

/**
* Response from the GitLab API for fetching a specific version(diff) of a merge
* request. Note that there is more information returned but we are not using it.
*/
interface DiffVersionsResponse {
base_commit_sha: string
}

type GetBaseCommitIDInput = Pick<GitLabDiffInfo, 'owner' | 'repoName' | 'mergeRequestID' | 'diffID'>

const buildURL = (owner: string, repoName: string, path: string) =>
`${window.location.origin}/api/v4/projects/${owner}%2f${repoName}${path}`

const get = <T>(url: string): Observable<T> => ajax.get(url).pipe(map(({ response }) => response as T))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid type parameters that are only used in the return type - it's only a cast in disguise. It's better to cast explicitly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting is actually what I intended with that. I think it's easier to read at the call sights.


/**
* Get the base commit ID for a merge request.
*/
export const getBaseCommitIDForMergeRequest: (info: GetBaseCommitIDInput) => Observable<string> = memoizeObservable(
({ owner, repoName, mergeRequestID, diffID }: GetBaseCommitIDInput) => {
const mrURL = buildURL(owner, repoName, `/merge_requests/${mergeRequestID}`)

// If we have a `diffID`, retrieve the information for that individual diff.
if (diffID) {
return get<DiffVersionsResponse>(`${mrURL}/versions/${diffID}`).pipe(
map(({ base_commit_sha }) => base_commit_sha)
)
}

// Otherwise, just get the overall base `commitID` for the merge request.
return get<MergeRequestResponse>(mrURL).pipe(map(({ diff_refs: { base_sha } }) => base_sha))
},
({ mergeRequestID, diffID }) => mergeRequestID + (diffID ? `/${diffID}` : '')
)

interface CommitResponse {
parent_ids: string[]
}

/**
* Get the base commit ID for a commit.
*/
export const getBaseCommitIDForCommit: (
{ owner, repoName, commitID }: Pick<GetBaseCommitIDInput, 'owner' | 'repoName'> & { commitID: string }
) => Observable<string> = memoizeObservable(({ owner, repoName, commitID }) =>
get<CommitResponse>(buildURL(owner, repoName, `/repository/commits/${commitID}`)).pipe(
map(({ parent_ids }) => first(parent_ids)!) // ! because it'll always have a parent if we are looking at the commit page.
)
)
Loading