261 lines
6.4 KiB
Go
261 lines
6.4 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package sourcegraph
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/cache"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
)
|
|
|
|
// Client wraps Sourcegraph GraphQL API
|
|
type Client struct {
|
|
baseURL string
|
|
accessToken string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// Range represents a range in source code
|
|
type Range struct {
|
|
Start Position `json:"start"`
|
|
End Position `json:"end"`
|
|
}
|
|
|
|
// Position represents a position in source code
|
|
type Position struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
}
|
|
|
|
// HoverResult represents hover documentation response
|
|
type HoverResult struct {
|
|
Contents string `json:"contents"` // Markdown content
|
|
Range *Range `json:"range,omitempty"`
|
|
}
|
|
|
|
// Location represents a code location
|
|
type Location struct {
|
|
Repo string `json:"repo"`
|
|
Path string `json:"path"`
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
var defaultClient *Client
|
|
|
|
// GetClient returns the singleton Sourcegraph client
|
|
func GetClient() *Client {
|
|
return defaultClient
|
|
}
|
|
|
|
// NewClient creates a new Sourcegraph client with current settings
|
|
func NewClient() *Client {
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: setting.Sourcegraph.SkipTLSVerify, //nolint:gosec
|
|
},
|
|
}
|
|
|
|
return &Client{
|
|
baseURL: setting.Sourcegraph.URL,
|
|
accessToken: setting.Sourcegraph.AccessToken,
|
|
httpClient: &http.Client{
|
|
Timeout: time.Duration(setting.Sourcegraph.Timeout) * time.Second,
|
|
Transport: transport,
|
|
},
|
|
}
|
|
}
|
|
|
|
// graphqlRequest performs a GraphQL request to Sourcegraph
|
|
func (c *Client) graphqlRequest(ctx context.Context, query string, variables map[string]any) (json.RawMessage, error) {
|
|
body := map[string]any{
|
|
"query": query,
|
|
"variables": variables,
|
|
}
|
|
bodyBytes, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/.api/graphql", bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.accessToken != "" {
|
|
req.Header.Set("Authorization", "token "+c.accessToken)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result struct {
|
|
Data json.RawMessage `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
if len(result.Errors) > 0 {
|
|
return nil, fmt.Errorf("graphql error: %s", result.Errors[0].Message)
|
|
}
|
|
|
|
return result.Data, nil
|
|
}
|
|
|
|
// cacheKey generates a cache key for code intelligence queries
|
|
func cacheKey(queryType, repo, rev, path string, line, char int) string {
|
|
return fmt.Sprintf("sg:%s:%s:%s:%s:%d:%d", queryType, repo, rev, path, line, char)
|
|
}
|
|
|
|
// Hover queries documentation at a position
|
|
func (c *Client) Hover(ctx context.Context, repo, rev, path string, line, char int) (*HoverResult, error) {
|
|
key := cacheKey("hover", repo, rev, path, line, char)
|
|
|
|
// Try cache first
|
|
if cached, hit := cache.GetCache().Get(key); hit {
|
|
var result HoverResult
|
|
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
|
return &result, nil
|
|
}
|
|
}
|
|
|
|
data, err := c.graphqlRequest(ctx, hoverQuery, map[string]any{
|
|
"repo": repo,
|
|
"rev": rev,
|
|
"path": path,
|
|
"line": line,
|
|
"char": char,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := parseHoverResponse(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the result
|
|
if result != nil {
|
|
if resultBytes, err := json.Marshal(result); err == nil {
|
|
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Definition queries for symbol definition
|
|
func (c *Client) Definition(ctx context.Context, repo, rev, path string, line, char int) ([]Location, error) {
|
|
key := cacheKey("def", repo, rev, path, line, char)
|
|
|
|
// Try cache first
|
|
if cached, hit := cache.GetCache().Get(key); hit {
|
|
var result []Location
|
|
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
data, err := c.graphqlRequest(ctx, definitionQuery, map[string]any{
|
|
"repo": repo,
|
|
"rev": rev,
|
|
"path": path,
|
|
"line": line,
|
|
"char": char,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := parseLocationsResponse(data, "definitions")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the result
|
|
if resultBytes, err := json.Marshal(result); err == nil {
|
|
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// References queries for all references to a symbol
|
|
func (c *Client) References(ctx context.Context, repo, rev, path string, line, char int) ([]Location, error) {
|
|
key := cacheKey("refs", repo, rev, path, line, char)
|
|
|
|
// Try cache first
|
|
if cached, hit := cache.GetCache().Get(key); hit {
|
|
var result []Location
|
|
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
data, err := c.graphqlRequest(ctx, referencesQuery, map[string]any{
|
|
"repo": repo,
|
|
"rev": rev,
|
|
"path": path,
|
|
"line": line,
|
|
"char": char,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := parseLocationsResponse(data, "references")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the result
|
|
if resultBytes, err := json.Marshal(result); err == nil {
|
|
_ = cache.GetCache().Put(key, string(resultBytes), int64(setting.Sourcegraph.CacheTTL))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// SyncRepository notifies Sourcegraph to re-sync a repository
|
|
func (c *Client) SyncRepository(ctx context.Context, repoName string) error {
|
|
// Use the repository sync mutation
|
|
_, err := c.graphqlRequest(ctx, syncRepoMutation, map[string]any{
|
|
"repo": repoName,
|
|
})
|
|
if err != nil {
|
|
log.Warn("Sourcegraph sync failed for %s: %v", repoName, err)
|
|
return err
|
|
}
|
|
log.Debug("Sourcegraph sync triggered for %s", repoName)
|
|
return nil
|
|
}
|