// 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) } log.Debug("Sourcegraph GraphQL request to %s: variables=%v", c.baseURL+"/.api/graphql", variables) 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 { log.Warn("Sourcegraph GraphQL error status %d: %s", resp.StatusCode, string(respBody)) return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody)) } log.Debug("Sourcegraph GraphQL response: %s", 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 { log.Warn("Sourcegraph GraphQL errors: %v", result.Errors) 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) log.Debug("Sourcegraph Hover request: repo=%s rev=%s path=%s line=%d char=%d", 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 { log.Debug("Sourcegraph Hover cache hit") 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 }