-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
provider for sourcegraph references endpoint
- Loading branch information
Showing
10 changed files
with
592 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Sourcegraph refs context provider for OpenCtx | ||
|
||
This is a context provider for [OpenCtx](https://openctx.org) that fetches Sourcegraph references for use as context. | ||
|
||
## Usage | ||
|
||
Add the following to your settings in any OpenCtx client: | ||
|
||
```json | ||
"openctx.providers": { | ||
// ...other providers... | ||
"https://openctx.org/npm/@openctx/provider-sourcegraph-refs": { | ||
"sourcegraphEndpoint": "https://sourcegraph.sourcegraph.com", | ||
"sourcegraphToken": "$YOUR_TOKEN", | ||
"repositoryNames": ["github.com/sourcegraph/sourcegraph"] | ||
} | ||
}, | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { escapeRegExp } from 'lodash' | ||
import { BLOB_QUERY, type BlobInfo, BlobResponseSchema } from './graphql_blobs.js' | ||
import { | ||
FUZZY_SYMBOLS_QUERY, | ||
FuzzySymbolsResponseSchema, | ||
type SymbolInfo, | ||
transformToSymbols, | ||
} from './graphql_symbols.js' | ||
import { | ||
USAGES_FOR_SYMBOL_QUERY, | ||
type Usage, | ||
UsagesForSymbolResponseSchema, | ||
transformToUsages, | ||
} from './graphql_usages.js' | ||
|
||
interface APIResponse<T> { | ||
data?: T | ||
errors?: { message: string; path?: string[] }[] | ||
} | ||
|
||
export class SourcegraphGraphQLAPIClient { | ||
constructor( | ||
private readonly endpoint: string, | ||
private readonly token: string, | ||
) {} | ||
|
||
public async fetchSymbols(query: string, repositories: string[]): Promise<SymbolInfo[] | Error> { | ||
const response: any | Error = await this.fetchSourcegraphAPI<APIResponse<any>>( | ||
FUZZY_SYMBOLS_QUERY, | ||
{ | ||
query: `type:symbol count:30 ${ | ||
repositories.length > 0 ? `repo:^(${repositories.map(escapeRegExp).join('|')})$` : '' | ||
} ${query}`, | ||
}, | ||
) | ||
|
||
if (isError(response)) { | ||
return response | ||
} | ||
|
||
try { | ||
const validatedData = FuzzySymbolsResponseSchema.parse(response.data) | ||
return transformToSymbols(validatedData) | ||
} catch (error) { | ||
return new Error(`Invalid response format: ${error}`) | ||
} | ||
} | ||
|
||
public async fetchUsages( | ||
repository: string, | ||
path: string, | ||
startLine: number, | ||
startCharacter: number, | ||
endLine: number, | ||
endCharacter: number, | ||
): Promise<Usage[] | Error> { | ||
const response: any | Error = await this.fetchSourcegraphAPI< | ||
APIResponse<typeof UsagesForSymbolResponseSchema> | ||
>(USAGES_FOR_SYMBOL_QUERY, { | ||
repository, | ||
path, | ||
startLine, | ||
startCharacter, | ||
endLine, | ||
endCharacter, | ||
}) | ||
|
||
if (isError(response)) { | ||
return response | ||
} | ||
|
||
try { | ||
// TODO(beyang): sort or filter by provenance | ||
const validatedData = UsagesForSymbolResponseSchema.parse(response.data) | ||
return transformToUsages(validatedData) | ||
} catch (error) { | ||
return new Error(`Invalid response format: ${error}`) | ||
} | ||
} | ||
|
||
public async fetchBlob({ | ||
repoName, | ||
revspec, | ||
path, | ||
startLine, | ||
endLine, | ||
}: { | ||
repoName: string | ||
revspec: string | ||
path: string | ||
startLine: number | ||
endLine: number | ||
}): Promise<BlobInfo | Error> { | ||
const response: any | Error = await this.fetchSourcegraphAPI<APIResponse<BlobInfo>>(BLOB_QUERY, { | ||
repoName, | ||
revspec, | ||
path, | ||
startLine, | ||
endLine, | ||
}) | ||
|
||
if (isError(response)) { | ||
return response | ||
} | ||
|
||
try { | ||
const validatedData = BlobResponseSchema.parse(response.data) | ||
return { | ||
repoName, | ||
revision: revspec, | ||
path: validatedData.repository.commit.blob.path, | ||
range: { | ||
start: { line: startLine, character: 0 }, | ||
end: { line: endLine, character: 0 }, | ||
}, | ||
content: validatedData.repository.commit.blob.content, | ||
} | ||
} catch (error) { | ||
return new Error(`Invalid response format: ${error}`) | ||
} | ||
} | ||
|
||
public async fetchSourcegraphAPI<T>( | ||
query: string, | ||
variables: Record<string, any> = {}, | ||
): Promise<T | Error> { | ||
const headers = new Headers() | ||
headers.set('Content-Type', 'application/json; charset=utf-8') | ||
headers.set('User-Agent', 'openctx-sourcegraph-search / 0.0.1') | ||
headers.set('Authorization', `token ${this.token}`) | ||
|
||
const queryName = query.match(/^\s*(?:query|mutation)\s+(\w+)/m)?.[1] ?? 'unknown' | ||
const url = this.endpoint + '/.api/graphql?' + queryName | ||
|
||
return fetch(url, { | ||
method: 'POST', | ||
body: JSON.stringify({ query, variables }), | ||
headers, | ||
}) | ||
.then(verifyResponseCode) | ||
.then(response => response.json() as T) | ||
.catch(error => new Error(`accessing Sourcegraph GraphQL API: ${error} (${url})`)) | ||
} | ||
} | ||
|
||
async function verifyResponseCode(response: Response): Promise<Response> { | ||
if (!response.ok) { | ||
const body = await response.text() | ||
throw new Error(`HTTP status code ${response.status}${body ? `: ${body}` : ''}`) | ||
} | ||
return response | ||
} | ||
|
||
export const isError = (value: unknown): value is Error => value instanceof Error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { z } from 'zod' | ||
|
||
export interface BlobInfo { | ||
repoName: string | ||
revision: string | ||
path: string | ||
range: { | ||
start: { line: number; character: number } | ||
end: { line: number; character: number } | ||
} | ||
content: string | ||
} | ||
|
||
export const BlobResponseSchema = z.object({ | ||
repository: z.object({ | ||
commit: z.object({ | ||
blob: z.object({ | ||
path: z.string(), | ||
url: z.string(), | ||
languages: z.array(z.string()), | ||
content: z.string(), | ||
}), | ||
}), | ||
}), | ||
}) | ||
|
||
export type BlobResponse = z.infer<typeof BlobResponseSchema> | ||
|
||
export const BLOB_QUERY = ` | ||
query Blob($repoName: String!, $revspec: String!, $path: String!, $startLine: Int!, $endLine: Int!) { | ||
repository(name: $repoName) { | ||
commit(rev: $revspec) { | ||
blob(path: $path) { | ||
path | ||
url | ||
languages | ||
content(startLine: $startLine, endLine: $endLine) | ||
} | ||
} | ||
} | ||
}` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { z } from 'zod' | ||
|
||
export interface SymbolInfo { | ||
name: string | ||
repositoryId: string | ||
repositoryName: string | ||
path: string | ||
range: { | ||
start: { line: number; character: number } | ||
end: { line: number; character: number } | ||
} | ||
} | ||
|
||
export const FuzzySymbolsResponseSchema = z.object({ | ||
search: z.object({ | ||
results: z.object({ | ||
results: z.array( | ||
z.object({ | ||
__typename: z.string(), | ||
file: z.object({ | ||
path: z.string(), | ||
}), | ||
symbols: z.array( | ||
z.object({ | ||
name: z.string(), | ||
location: z.object({ | ||
range: z.object({ | ||
start: z.object({ line: z.number(), character: z.number() }), | ||
end: z.object({ line: z.number(), character: z.number() }), | ||
}), | ||
resource: z.object({ | ||
path: z.string(), | ||
}), | ||
}), | ||
}), | ||
), | ||
repository: z.object({ | ||
id: z.string(), | ||
name: z.string(), | ||
}), | ||
}), | ||
), | ||
}), | ||
}), | ||
}) | ||
|
||
export function transformToSymbols(response: z.infer<typeof FuzzySymbolsResponseSchema>): SymbolInfo[] { | ||
return response.search.results.results.flatMap(result => { | ||
return (result.symbols || []).map(symbol => ({ | ||
name: symbol.name, | ||
repositoryId: result.repository.id, | ||
repositoryName: result.repository.name, | ||
path: symbol.location.resource.path, | ||
range: { | ||
start: { | ||
line: symbol.location.range.start.line, | ||
character: symbol.location.range.start.character, | ||
}, | ||
end: { | ||
line: symbol.location.range.end.line, | ||
character: symbol.location.range.end.character, | ||
}, | ||
}, | ||
})) | ||
}) | ||
} | ||
|
||
export const FUZZY_SYMBOLS_QUERY = ` | ||
query FuzzySymbols($query: String!) { | ||
search(patternType: regexp, query: $query) { | ||
results { | ||
results { | ||
... on FileMatch { | ||
__typename | ||
file { | ||
path | ||
} | ||
symbols { | ||
name | ||
location { | ||
range { | ||
start { line, character} | ||
end { line, character } | ||
} | ||
resource { | ||
path | ||
} | ||
} | ||
} | ||
repository { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
` |
Oops, something went wrong.