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

Commit 590f930

Browse files
committed
feat(review-board): add review board support
1 parent 06f6f75 commit 590f930

File tree

11 files changed

+502
-10
lines changed

11 files changed

+502
-10
lines changed

app/components/CodeIntelStatusIndicator.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class CodeIntelStatusIndicator extends React.Component<
213213
<>
214214
<h3>No language server connected</h3>
215215
Check{' '}
216-
<a href="http://langserver.org/" target="_blank">
216+
<a className="text-primary" href="http://langserver.org/" target="_blank">
217217
langserver.org
218218
</a>{' '}
219219
for {language} language servers
@@ -248,7 +248,11 @@ export class CodeIntelStatusIndicator extends React.Component<
248248
<>
249249
<h3>
250250
Connected to the <wbr />
251-
<a href={this.state.langServerOrError.homepageURL} target="_blank">
251+
<a
252+
className="text-primary"
253+
href={this.state.langServerOrError.homepageURL}
254+
target="_blank"
255+
>
252256
{this.state.langServerOrError.displayName || language} language server
253257
</a>
254258
</h3>
@@ -307,18 +311,28 @@ export class CodeIntelStatusIndicator extends React.Component<
307311
)}
308312
{this.props.userIsSiteAdmin && (
309313
<p className="mt-2 mb-0">
310-
<a href={`${sourcegraphUrl}/site-admin/code-intelligence`}>Manage</a>
314+
<a
315+
className="text-primary"
316+
href={`${sourcegraphUrl}/site-admin/code-intelligence`}
317+
>
318+
Manage
319+
</a>
311320
</p>
312321
)}
313322
{this.state.langServerOrError.issuesURL && (
314323
<p className="mt-2 mb-0">
315-
<a href={this.state.langServerOrError.issuesURL} target="_blank">
324+
<a
325+
className="text-primary"
326+
href={this.state.langServerOrError.issuesURL}
327+
target="_blank"
328+
>
316329
Report issue
317330
</a>
318331
</p>
319332
)}
320333
<p className="mt-2 mb-0">
321334
<a
335+
className="text-primary"
322336
onClick={this.toggleCodeIntelligence}
323337
style={{ cursor: 'pointer' }}
324338
target="_blank"

app/components/codeIntelStatusIndicator.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
&__unstyled-list {
2323
list-style-type: none;
24+
padding: 0;
2425
}
2526
}
2627

app/repo/backend.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export const resolveRepo = memoizeObservable(
100100
getContext({ repoKey: ctx.repoPath }),
101101
`query ResolveRepo($repoPath: String!) {
102102
repository(uri: $repoPath) {
103-
url
103+
name
104104
}
105105
}`,
106106
{ ...ctx }
@@ -109,8 +109,7 @@ export const resolveRepo = memoizeObservable(
109109
if (!result.data || !result.data.repository) {
110110
throw new RepoNotFoundError(ctx.repoPath)
111111
}
112-
113-
return result.data.repository.url
112+
return result.data.repository.name
114113
}, catchError((err, caught) => caught))
115114
),
116115
makeRepoURI

app/repo/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,25 @@ export function makeRepoURI(parsed: ParsedRepoURI): RepoURI {
300300
return uri
301301
}
302302

303+
export function normalizeRepoPath(origin: string): string {
304+
let repoPath = origin
305+
repoPath = repoPath.replace('\\', '')
306+
if (origin.startsWith('git@')) {
307+
repoPath = origin.substr('git@'.length)
308+
repoPath = repoPath.replace(':', '/')
309+
} else if (origin.startsWith('git://')) {
310+
repoPath = origin.substr('git://'.length)
311+
} else if (origin.startsWith('https://')) {
312+
repoPath = origin.substr('https://'.length)
313+
} else if (origin.includes('@')) {
314+
// Assume the origin looks like `username@host:repo/path`
315+
const split = origin.split('@')
316+
repoPath = split[1]
317+
repoPath = repoPath.replace(':', '/')
318+
}
319+
return repoPath.replace(/.git$/, '')
320+
}
321+
303322
/**
304323
* A file at an exact commit of a known programming language
305324
*/

app/review-board/backend/fetch.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { from, Observable } from 'rxjs'
2+
import { ReviewBoardRepository } from '../util'
3+
4+
function getRepository(repoID: number): Promise<ReviewBoardRepository> {
5+
return new Promise((resolve, reject) => {
6+
fetch(`${window.location.origin}/api/repositories/${repoID}/`, {
7+
method: 'GET',
8+
credentials: 'include',
9+
headers: new Headers({ Accept: 'application/json' }),
10+
})
11+
.then(resp => resp.json())
12+
.then(resp => resolve(resp.repository))
13+
.catch(err => reject(err))
14+
})
15+
}
16+
17+
export function getRepositoryFromReviewBoardAPI(repoID: number): Observable<ReviewBoardRepository> {
18+
return from(getRepository(repoID))
19+
}

app/review-board/dom/util.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { DiffPart, DOMFunctions } from '@sourcegraph/codeintellify'
2+
3+
/**
4+
* Gets the `<td>` element for a target that contains the code
5+
*/
6+
const getCodeCellFromTarget = (target: HTMLElement): HTMLElement | null => {
7+
if (target.nodeName === 'PRE') {
8+
return null
9+
}
10+
const pre = target.closest('pre') as HTMLElement
11+
if (!pre) {
12+
return null
13+
}
14+
if (target.innerText.trim().length === 0) {
15+
return null
16+
}
17+
const closest = (target.closest('.l') || target.closest('.r')) as HTMLElement
18+
if (!closest.classList.contains('trimmed') && closest.classList.contains('annotated')) {
19+
const firstChild = closest.firstElementChild
20+
if (
21+
firstChild &&
22+
firstChild.nodeName === 'SPAN' &&
23+
firstChild.textContent &&
24+
firstChild.innerHTML.trim().length === 0
25+
) {
26+
const newElement = document.createElement('span')
27+
newElement.innerHTML = ' '
28+
closest.replaceChild(newElement, firstChild)
29+
}
30+
closest.classList.add('trimmed')
31+
}
32+
return closest
33+
}
34+
35+
const getBlobCodeInner = (codeCell: HTMLElement) => {
36+
if (codeCell.classList.contains('l') || codeCell.classList.contains('r')) {
37+
return codeCell
38+
}
39+
return (codeCell.closest('.l') || codeCell.closest('.r')) as HTMLElement
40+
}
41+
42+
/**
43+
* Gets the line number for a given code element on unified diff, split diff and blob views
44+
*/
45+
const getLineNumberFromCodeElement = (codeElement: HTMLElement): number => {
46+
// In diff views, the code element is the `<span>` inside the cell
47+
// On blob views, the code element is the `<td>` itself, so `closest()` will simply return it
48+
// Walk all previous sibling cells until we find one with the line number
49+
let cell = codeElement.closest('td') as HTMLElement
50+
while (cell) {
51+
if (cell.nodeName === 'TH') {
52+
return parseInt(cell.innerText, 10)
53+
}
54+
cell = cell.previousElementSibling as HTMLTableRowElement
55+
}
56+
57+
cell = codeElement.closest('tr')! as HTMLTableRowElement
58+
if (cell.getAttribute('line')) {
59+
return parseInt(cell.getAttribute('line')!, 10)
60+
}
61+
throw new Error('Could not find a line number in any cell')
62+
}
63+
64+
/**
65+
* getDeltaFileName returns the path of the file container. Reviewboard will always be a diff.
66+
*/
67+
export function getDeltaFileName(container: HTMLElement): { headFilePath: string; baseFilePath: string } {
68+
const info = container.querySelector('.filename-row') as HTMLElement
69+
if (!info) {
70+
throw new Error(`Unable to getDeltaFileName for container: ${container}`)
71+
}
72+
return { headFilePath: info.innerText, baseFilePath: info.innerText }
73+
}
74+
75+
const getDiffCodePart = (codeElement: HTMLElement): DiffPart => {
76+
const td = codeElement.closest('td')!
77+
// If there are more cells on the right, this is the base, otherwise the head
78+
return td.classList.contains('l') ? 'base' : 'head'
79+
}
80+
81+
/**
82+
* Implementations of the DOM functions for diff code views
83+
*/
84+
export const diffDomFunctions: DOMFunctions = {
85+
getCodeElementFromTarget: target => {
86+
const codeCell = getCodeCellFromTarget(target)
87+
return codeCell && getBlobCodeInner(codeCell)
88+
},
89+
getCodeElementFromLineNumber: () => null,
90+
getLineNumberFromCodeElement,
91+
getDiffCodePart,
92+
}
93+
94+
/**
95+
* createBlobAnnotatorMount creates a <div> element and adds it to the DOM
96+
* where the BlobAnnotator component should be mounted.
97+
*/
98+
export function createBlobAnnotatorMount(fileContainer: HTMLElement, isBase?: boolean): HTMLElement | null {
99+
const className = 'sourcegraph-app-annotator' + (isBase ? '-base' : '')
100+
const existingMount = fileContainer.querySelector('.' + className) as HTMLElement
101+
if (existingMount) {
102+
return existingMount
103+
}
104+
105+
const mountEl = document.createElement('div')
106+
mountEl.style.display = 'inline-flex'
107+
mountEl.style.verticalAlign = 'middle'
108+
mountEl.style.alignItems = 'center'
109+
mountEl.className = className
110+
mountEl.style.cssFloat = 'right'
111+
112+
const fileActions = fileContainer.querySelector('.filename-row') as HTMLElement
113+
if (!fileActions || !fileActions.firstElementChild) {
114+
return null
115+
}
116+
;(fileActions.firstElementChild as HTMLElement).style.overflow = 'visible'
117+
fileActions.firstElementChild!.appendChild(mountEl)
118+
return mountEl
119+
}

0 commit comments

Comments
 (0)