Skip to content

Commit

Permalink
provider for usage examples from sourcegraph api (#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
beyang authored Jan 22, 2025
1 parent c6fa133 commit 2fe038e
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 0 deletions.
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions provider/sourcegraph-refs/README.md
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.com",
"sourcegraphToken": "$YOUR_TOKEN",
"repositoryNames": ["github.com/sourcegraph/cody"]
}
},
```
154 changes: 154 additions & 0 deletions provider/sourcegraph-refs/graphql.ts
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
41 changes: 41 additions & 0 deletions provider/sourcegraph-refs/graphql_blobs.ts
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)
}
}
}
}`
99 changes: 99 additions & 0 deletions provider/sourcegraph-refs/graphql_symbols.ts
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
}
}
}
}
}
}
`
Loading

0 comments on commit 2fe038e

Please sign in to comment.