Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@ jobs:
- name: Install golangci-lint
uses: golangci/golangci-lint-action@v9
with:
args: ./...
- name: Run go vet
install-only: true
- name: Lint
run: make lint-go
- name: Vet
run: make vet
- name: Run tests
- name: Test
env:
CS_DATABASE_URL: postgres://codesearch:codesearch@localhost:5432/codesearch?sslmode=disable
CS_REDIS_ADDR: localhost:6379
run: make test
run: make test-go

web-checks:
needs: changes
Expand All @@ -95,9 +97,9 @@ jobs:
- name: Install dependencies
run: cd web && bun install
- name: Lint
run: cd web && bun lint
run: make lint-web
- name: Test
run: cd web && bun run test:run
run: make test-web

website-checks:
needs: changes
Expand All @@ -109,7 +111,7 @@ jobs:
- name: Install dependencies
run: cd website && bun install
- name: Build
run: cd website && bun run build
run: make build-website

docker-build:
needs: changes
Expand Down
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ linters:

exclusions:
generated: lax
paths:
- web
- website
rules:
- path: _test\.go
linters:
Expand Down
32 changes: 27 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build clean test lint help \
.PHONY: all build build-website clean test test-go test-web lint lint-go lint-web fmt fmt-go fmt-web vet help \
release-api release-indexer release-web release-zoekt release-website \
release-helm release-helm-website release-cli

Expand All @@ -24,14 +24,17 @@ DEV_ENV := CS_DATABASE_URL=postgres://codesearch:codesearch@localhost:5432/codes

all: build

build: ## Build all binaries
build: ## Build all Go binaries
$(GOCMD) build -o $(BIN_DIR)/code-search ./$(CMD_DIR)/cli
$(GOCMD) build -o $(BIN_DIR)/api-server ./$(CMD_DIR)/api
$(GOCMD) build -o $(BIN_DIR)/indexer ./$(CMD_DIR)/indexer
$(GOCMD) build -o $(BIN_DIR)/migrate ./$(CMD_DIR)/migrate
$(GOCMD) build -o $(BIN_DIR)/zoekt-refresh ./$(CMD_DIR)/zoekt-refresh
$(GOCMD) build -o $(BIN_DIR)/mcp-server ./$(CMD_DIR)/mcp

build-website: ## Build project website
cd website && bun run build

# =============================================================================
# Development
# =============================================================================
Expand Down Expand Up @@ -85,16 +88,35 @@ dev-setup: ## First-time setup for local development
# Testing & Linting
# =============================================================================

test: ## Run tests
test: test-go test-web ## Run all tests

test-go: ## Run Go tests
$(GOCMD) test -v ./...

test-cover: ## Run tests with coverage
test-web: ## Run web frontend tests
cd web && bun run test:run

test-cover: ## Run Go tests with coverage
$(GOCMD) test -v -cover -coverprofile=coverage.out ./...
$(GOCMD) tool cover -html=coverage.out -o coverage.html

lint: ## Run linters
lint: lint-go lint-web ## Run all linters

lint-go: ## Run Go linters
golangci-lint run ./...

lint-web: ## Run web frontend linter
cd web && bun lint

fmt: fmt-go fmt-web ## Format all code

fmt-go: ## Format Go code
gofmt -w .
golines -w .

fmt-web: ## Format web frontend code (prettier)
cd web && bun run format

vet: ## Run go vet
$(GOCMD) vet ./...

Expand Down
12 changes: 10 additions & 2 deletions cmd/api/handlers/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,11 @@ func (h *Handler) GetBranchesAndTags(w http.ResponseWriter, r *http.Request) {
return
}

if !h.services.Authorizer.CanAccessRepo(r.Context(), middleware.UserFromContext(r.Context()), repo.Name) {
if !h.services.Authorizer.CanAccessRepo(
r.Context(),
middleware.UserFromContext(r.Context()),
repo.Name,
) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
Expand Down Expand Up @@ -461,7 +465,11 @@ func (h *Handler) GetFileSymbols(w http.ResponseWriter, r *http.Request) {
return
}

if !h.services.Authorizer.CanAccessRepo(r.Context(), middleware.UserFromContext(r.Context()), repo.Name) {
if !h.services.Authorizer.CanAccessRepo(
r.Context(),
middleware.UserFromContext(r.Context()),
repo.Name,
) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
Expand Down
24 changes: 20 additions & 4 deletions cmd/api/handlers/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@ func (h *Handler) GetRepoByID(w http.ResponseWriter, r *http.Request) {
return
}

if !h.services.Authorizer.CanAccessRepo(r.Context(), middleware.UserFromContext(r.Context()), repo.Name) {
if !h.services.Authorizer.CanAccessRepo(
r.Context(),
middleware.UserFromContext(r.Context()),
repo.Name,
) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
Expand Down Expand Up @@ -463,7 +467,11 @@ func (h *Handler) IncludeRepoByID(w http.ResponseWriter, r *http.Request) {
acquired, err := h.services.Queue.TryAcquireIndexJob(r.Context(), repo.ID)
if err != nil {
// Log but don't fail - queue check is best-effort
log.L.Debug("Failed to acquire index job slot", zap.Int64("repo_id", repo.ID), zap.Error(err))
log.L.Debug(
"Failed to acquire index job slot",
zap.Int64("repo_id", repo.ID),
zap.Error(err),
)
}

var jobID string
Expand Down Expand Up @@ -548,7 +556,11 @@ func (h *Handler) RestoreRepoByID(w http.ResponseWriter, r *http.Request) {
// Queue an index job immediately so it doesn't wait for scheduler
acquired, err := h.services.Queue.TryAcquireIndexJob(r.Context(), repo.ID)
if err != nil {
log.L.Debug("Failed to acquire index job slot", zap.Int64("repo_id", repo.ID), zap.Error(err))
log.L.Debug(
"Failed to acquire index job slot",
zap.Int64("repo_id", repo.ID),
zap.Error(err),
)
}

var jobID string
Expand Down Expand Up @@ -630,7 +642,11 @@ func (h *Handler) SyncRepoByID(w http.ResponseWriter, r *http.Request) {
if repo.IndexStatus == "pending" {
active, err := h.services.Queue.HasPendingIndexJob(r.Context(), repo.ID)
if err != nil {
log.L.Debug("Failed to check active index job", zap.Int64("repo_id", repo.ID), zap.Error(err))
log.L.Debug(
"Failed to check active index job",
zap.Int64("repo_id", repo.ID),
zap.Error(err),
)

active = true
}
Expand Down
17 changes: 14 additions & 3 deletions cmd/api/handlers/scip.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ func NewSCIPHandler(services *Services, scipSvc *scip.Service) *SCIPHandler {
}

// getBlob fetches file content using federated access when available.
func (h *SCIPHandler) getBlob(ctx context.Context, repoName, path, ref string) (*files.BlobResponse, error) {
func (h *SCIPHandler) getBlob(
ctx context.Context,
repoName, path, ref string,
) (*files.BlobResponse, error) {
if h.services.FederatedFiles != nil {
return h.services.FederatedFiles.GetBlob(ctx, repoName, path, ref)
}
Expand Down Expand Up @@ -116,7 +119,11 @@ func (h *SCIPHandler) checkRepoAccess(w http.ResponseWriter, r *http.Request, re
return false
}

if !h.services.Authorizer.CanAccessRepo(r.Context(), middleware.UserFromContext(r.Context()), repo.Name) {
if !h.services.Authorizer.CanAccessRepo(
r.Context(),
middleware.UserFromContext(r.Context()),
repo.Name,
) {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
Expand Down Expand Up @@ -352,7 +359,11 @@ func (h *SCIPHandler) IndexRepository(w http.ResponseWriter, r *http.Request) {
return
}

if !h.services.Authorizer.CanAccessRepo(r.Context(), middleware.UserFromContext(r.Context()), repo.Name) {
if !h.services.Authorizer.CanAccessRepo(
r.Context(),
middleware.UserFromContext(r.Context()),
repo.Name,
) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/api/handlers/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,11 @@ func (h *Handler) SearchSuggestions(w http.ResponseWriter, r *http.Request) {
Example: "content:FOO",
},
{Keyword: "branch:", Description: "Filter by branch/tag", Example: "branch:main"},
{Keyword: "type:", Description: "Result type: filematch, filename, or repo", Example: "type:filename main"},
{
Keyword: "type:",
Description: "Result type: filematch, filename, or repo",
Example: "type:filename main",
},
{Keyword: "regex:", Description: "Treat pattern as regex", Example: "regex:func\\s+main"},
{Keyword: "-repo:", Description: "Exclude repository", Example: "-repo:test"},
{Keyword: "-file:", Description: "Exclude file pattern", Example: "-file:*_test.go"},
Expand Down
19 changes: 16 additions & 3 deletions cmd/api/handlers/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ func (h *Handler) FindSymbols(w http.ResponseWriter, r *http.Request) {

// RBAC: filter requested repos to only those the user can access
if len(req.Repos) > 0 {
req.Repos = h.services.Authorizer.FilterRepos(r.Context(), middleware.UserFromContext(r.Context()), req.Repos)
req.Repos = h.services.Authorizer.FilterRepos(
r.Context(),
middleware.UserFromContext(r.Context()),
req.Repos,
)
}

// Build Zoekt query with sym: filter for symbol search
Expand Down Expand Up @@ -160,7 +164,11 @@ func (h *Handler) FindRefs(w http.ResponseWriter, r *http.Request) {

// RBAC: filter requested repos to only those the user can access
if len(req.Repos) > 0 {
req.Repos = h.services.Authorizer.FilterRepos(r.Context(), middleware.UserFromContext(r.Context()), req.Repos)
req.Repos = h.services.Authorizer.FilterRepos(
r.Context(),
middleware.UserFromContext(r.Context()),
req.Repos,
)
}

// Build Zoekt query - search for the symbol as a literal string
Expand Down Expand Up @@ -558,7 +566,12 @@ func (h *Handler) TestConnection(w http.ResponseWriter, r *http.Request) {
// The actual fetching is done asynchronously by the indexer worker.
func (h *Handler) SyncConnection(w http.ResponseWriter, r *http.Request) {
if h.connectionsReadOnly {
http.Error(w, "Connections are read-only. Manage connections via config file.", http.StatusForbidden)
http.Error(
w,
"Connections are read-only. Manage connections via config file.",
http.StatusForbidden,
)

return
}

Expand Down
6 changes: 5 additions & 1 deletion cmd/api/handlers/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
}

if repoName == "" {
http.Error(w, "Could not extract repository name from webhook payload", http.StatusBadRequest)
http.Error(
w,
"Could not extract repository name from webhook payload",
http.StatusBadRequest,
)
return
}

Expand Down
4 changes: 3 additions & 1 deletion cmd/api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,9 @@ func (b *Builder) WithAuditLogger(al audit.AuditLogger) *Builder {
// WithSearchResultFilter sets a function to filter search results based on
// user access. This is a convenience that wraps authorizer-level filtering
// into the search pipeline.
func (b *Builder) WithSearchResultFilter(fn func(ctx context.Context, results []search.SearchResult) []search.SearchResult) *Builder {
func (b *Builder) WithSearchResultFilter(
fn func(ctx context.Context, results []search.SearchResult) []search.SearchResult,
) *Builder {
b.services.SearchResultFilter = fn
return b
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/api/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ func TestSecurityHeaders_PresentOnAllMethods(t *testing.T) {
})
handler := securityHeaders(inner)

methods := []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions}
methods := []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
http.MethodOptions,
}
for _, method := range methods {
req := httptest.NewRequest(method, "/", nil)
rr := httptest.NewRecorder()
Expand Down
5 changes: 4 additions & 1 deletion cmd/cli/cmd/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,10 @@ func runRepoRemove(cmd *cobra.Command, args []string) error {
force, _ := cmd.Flags().GetBool("force")

if !force {
fmt.Printf("Remove repository ID '%d'? This will soft-delete the repository (can be restored later). [y/N]: ", repoID)
fmt.Printf(
"Remove repository ID '%d'? This will soft-delete the repository (can be restored later). [y/N]: ",
repoID,
)

reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
Expand Down
6 changes: 4 additions & 2 deletions cmd/cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ func init() {
rootCmd.PersistentFlags().
StringVar(&apiURL, "api-url", "http://localhost:8080", "API server URL")
rootCmd.PersistentFlags().String("output", "text", "Output format: text, json, table")
rootCmd.PersistentFlags().String("token", "", "Code host token for replace operations (GitHub/GitLab PAT)")
rootCmd.PersistentFlags().String("auth-token", "", "Authentication token (JWT or API token, env: CODE_SEARCH_AUTH_TOKEN)")
rootCmd.PersistentFlags().
String("token", "", "Code host token for replace operations (GitHub/GitLab PAT)")
rootCmd.PersistentFlags().
String("auth-token", "", "Authentication token (JWT or API token, env: CODE_SEARCH_AUTH_TOKEN)")
rootCmd.PersistentFlags().
StringVar(&iapClientID, "iap-client-id", "", "IAP OAuth client ID (for servers behind Google IAP)")

Expand Down
11 changes: 8 additions & 3 deletions cmd/cli/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ func outputSearchText(resp *client.SearchResponse) error {
fileKey := result.Repo + "/" + result.File

last := len(groups) - 1
if last >= 0 && groups[last].result.Repo+"/"+groups[last].result.File == fileKey && groups[last].result.Line == result.Line {
groups[last].ranges = append(groups[last].ranges, [2]int{result.MatchStart, result.MatchEnd})
if last >= 0 && groups[last].result.Repo+"/"+groups[last].result.File == fileKey &&
groups[last].result.Line == result.Line {
groups[last].ranges = append(
groups[last].ranges,
[2]int{result.MatchStart, result.MatchEnd},
)
} else {
groups = append(groups, lineGroup{
result: result,
Expand Down Expand Up @@ -350,7 +354,8 @@ func runStreamSearch(c *client.Client, req *client.SearchRequest, output string)
fileKey := result.Repo + "/" + result.File

// Check if this is a same-line match we can merge
if pendingResult != nil && pendingResult.Repo+"/"+pendingResult.File == fileKey && pendingResult.Line == result.Line {
if pendingResult != nil && pendingResult.Repo+"/"+pendingResult.File == fileKey &&
pendingResult.Line == result.Line {
pendingRanges = append(pendingRanges, [2]int{result.MatchStart, result.MatchEnd})
return nil
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/indexer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ func main() {
}
}

svc, scipErr := scip.NewServiceWithConfig(scipCacheDir, indexerCfg, log.L.With(zap.String("component", "scip")))
svc, scipErr := scip.NewServiceWithConfig(
scipCacheDir,
indexerCfg,
log.L.With(zap.String("component", "scip")),
)
if scipErr != nil {
log.Warn("Failed to initialize SCIP service, auto-indexing disabled", log.Err(scipErr))
} else {
Expand Down
Loading