mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add branch isolation for codebase indexing
- Add codebaseIndexBranchIsolation configuration option - Update QdrantVectorStore to use branch-specific collection names - Add Git branch detection utilities (getCurrentBranch, sanitizeBranchName) - Update UI with branch isolation toggle in Advanced Configuration - Add i18n translations for new UI elements - Add comprehensive tests for branch isolation functionality When enabled, each Git branch will have its own separate index collection, ensuring search results always reflect the current branch's code. Addresses #8565
This commit is contained in:
parent
eeaafef786
commit
f34233be29
9 changed files with 481 additions and 5 deletions
|
|
@ -20,6 +20,7 @@ export const CODEBASE_INDEX_DEFAULTS = {
|
|||
|
||||
export const codebaseIndexConfigSchema = z.object({
|
||||
codebaseIndexEnabled: z.boolean().optional(),
|
||||
codebaseIndexBranchIsolation: z.boolean().optional(),
|
||||
codebaseIndexQdrantUrl: z.string().optional(),
|
||||
codebaseIndexEmbedderProvider: z
|
||||
.enum(["openai", "ollama", "openai-compatible", "gemini", "mistral", "vercel-ai-gateway"])
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "..
|
|||
*/
|
||||
export class CodeIndexConfigManager {
|
||||
private codebaseIndexEnabled: boolean = true
|
||||
private codebaseIndexBranchIsolation: boolean = false
|
||||
private embedderProvider: EmbedderProvider = "openai"
|
||||
private modelId?: string
|
||||
private modelDimension?: number
|
||||
|
|
@ -45,6 +46,7 @@ export class CodeIndexConfigManager {
|
|||
// Load configuration from storage
|
||||
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
|
||||
codebaseIndexEnabled: true,
|
||||
codebaseIndexBranchIsolation: false,
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
codebaseIndexEmbedderBaseUrl: "",
|
||||
|
|
@ -55,6 +57,7 @@ export class CodeIndexConfigManager {
|
|||
|
||||
const {
|
||||
codebaseIndexEnabled,
|
||||
codebaseIndexBranchIsolation,
|
||||
codebaseIndexQdrantUrl,
|
||||
codebaseIndexEmbedderProvider,
|
||||
codebaseIndexEmbedderBaseUrl,
|
||||
|
|
@ -74,6 +77,7 @@ export class CodeIndexConfigManager {
|
|||
|
||||
// Update instance variables with configuration
|
||||
this.codebaseIndexEnabled = codebaseIndexEnabled ?? true
|
||||
this.codebaseIndexBranchIsolation = codebaseIndexBranchIsolation ?? false
|
||||
this.qdrantUrl = codebaseIndexQdrantUrl
|
||||
this.qdrantApiKey = qdrantApiKey ?? ""
|
||||
this.searchMinScore = codebaseIndexSearchMinScore
|
||||
|
|
@ -156,6 +160,7 @@ export class CodeIndexConfigManager {
|
|||
// Capture the ACTUAL previous state before loading new configuration
|
||||
const previousConfigSnapshot: PreviousConfigSnapshot = {
|
||||
enabled: this.codebaseIndexEnabled,
|
||||
branchIsolation: this.codebaseIndexBranchIsolation,
|
||||
configured: this.isConfigured(),
|
||||
embedderProvider: this.embedderProvider,
|
||||
modelId: this.modelId,
|
||||
|
|
@ -259,6 +264,7 @@ export class CodeIndexConfigManager {
|
|||
|
||||
// Handle null/undefined values safely
|
||||
const prevEnabled = prev?.enabled ?? false
|
||||
const prevBranchIsolation = prev?.branchIsolation ?? false
|
||||
const prevConfigured = prev?.configured ?? false
|
||||
const prevProvider = prev?.embedderProvider ?? "openai"
|
||||
const prevOpenAiKey = prev?.openAiKey ?? ""
|
||||
|
|
@ -282,6 +288,11 @@ export class CodeIndexConfigManager {
|
|||
return true
|
||||
}
|
||||
|
||||
// 2.5. Branch isolation setting changed
|
||||
if (prevBranchIsolation !== this.codebaseIndexBranchIsolation) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 3. If wasn't ready before and isn't ready now, no restart needed
|
||||
if ((!prevEnabled || !prevConfigured) && (!this.codebaseIndexEnabled || !nowConfigured)) {
|
||||
return false
|
||||
|
|
@ -480,4 +491,12 @@ export class CodeIndexConfigManager {
|
|||
public get currentSearchMaxResults(): number {
|
||||
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether branch isolation is enabled for codebase indexing.
|
||||
* When enabled, each Git branch will have its own separate index.
|
||||
*/
|
||||
public get isBranchIsolationEnabled(): boolean {
|
||||
return this.codebaseIndexBranchIsolation
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface CodeIndexConfig {
|
|||
*/
|
||||
export type PreviousConfigSnapshot = {
|
||||
enabled: boolean
|
||||
branchIsolation: boolean
|
||||
configured: boolean
|
||||
embedderProvider: EmbedderProvider
|
||||
modelId?: string
|
||||
|
|
|
|||
|
|
@ -145,8 +145,15 @@ export class CodeIndexServiceFactory {
|
|||
throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing"))
|
||||
}
|
||||
|
||||
// Assuming constructor is updated: new QdrantVectorStore(workspacePath, url, vectorSize, apiKey?)
|
||||
return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, vectorSize, config.qdrantApiKey)
|
||||
// Pass branch isolation setting to QdrantVectorStore
|
||||
const branchIsolationEnabled = this.configManager.isBranchIsolationEnabled
|
||||
return new QdrantVectorStore(
|
||||
this.workspacePath,
|
||||
config.qdrantUrl,
|
||||
vectorSize,
|
||||
config.qdrantApiKey,
|
||||
branchIsolationEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
import { describe, it, expect, beforeEach, vi } from "vitest"
|
||||
import { QdrantClient } from "@qdrant/js-client-rest"
|
||||
import { QdrantVectorStore } from "../qdrant-client"
|
||||
import * as gitUtils from "../../../../utils/git"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@qdrant/js-client-rest")
|
||||
vi.mock("crypto", () => ({
|
||||
createHash: vi.fn(() => ({
|
||||
update: vi.fn(() => ({
|
||||
digest: vi.fn(() => "mockedhash1234567890abcdef"),
|
||||
})),
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock("../../../../utils/git", () => ({
|
||||
getCurrentBranch: vi.fn(),
|
||||
sanitizeBranchName: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("QdrantVectorStore - Branch Isolation", () => {
|
||||
let mockQdrantClientInstance: any
|
||||
const mockWorkspacePath = "/test/workspace"
|
||||
const mockQdrantUrl = "http://localhost:6333"
|
||||
const mockVectorSize = 1536
|
||||
const mockApiKey = "test-api-key"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockQdrantClientInstance = {
|
||||
getCollection: vi.fn(),
|
||||
createCollection: vi.fn(),
|
||||
deleteCollection: vi.fn(),
|
||||
createPayloadIndex: vi.fn(),
|
||||
upsert: vi.fn(),
|
||||
query: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}
|
||||
;(QdrantClient as any).mockImplementation(() => mockQdrantClientInstance)
|
||||
})
|
||||
|
||||
describe("Branch Isolation Disabled", () => {
|
||||
it("should use workspace-only collection name when branch isolation is disabled", async () => {
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
false, // branchIsolationEnabled = false
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should create collection with workspace-only name
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456",
|
||||
expect.any(Object),
|
||||
)
|
||||
expect(gitUtils.getCurrentBranch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return null for getCurrentBranch when branch isolation is disabled", () => {
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
false,
|
||||
)
|
||||
|
||||
expect(vectorStore.getCurrentBranch()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Branch Isolation Enabled", () => {
|
||||
it("should use branch-specific collection name when on a Git branch", async () => {
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue("feature/test-branch")
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue("feature-test-branch")
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true, // branchIsolationEnabled = true
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should detect current branch
|
||||
expect(gitUtils.getCurrentBranch).toHaveBeenCalledWith(mockWorkspacePath)
|
||||
expect(gitUtils.sanitizeBranchName).toHaveBeenCalledWith("feature/test-branch")
|
||||
|
||||
// Should create collection with branch-specific name
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456-br-feature-test-branch",
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("should use workspace-only collection name when in detached HEAD state", async () => {
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue(null) // Detached HEAD
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should detect detached HEAD
|
||||
expect(gitUtils.getCurrentBranch).toHaveBeenCalledWith(mockWorkspacePath)
|
||||
|
||||
// Should create collection with workspace-only name (no branch suffix)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456",
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("should use workspace-only collection name when not in a Git repository", async () => {
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue(null) // Not a Git repo
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should detect not a Git repo
|
||||
expect(gitUtils.getCurrentBranch).toHaveBeenCalledWith(mockWorkspacePath)
|
||||
|
||||
// Should create collection with workspace-only name (no branch suffix)
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456",
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle branch names with special characters", async () => {
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue("feature/user-auth-2.0")
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue("feature-user-auth-2-0")
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should sanitize branch name
|
||||
expect(gitUtils.sanitizeBranchName).toHaveBeenCalledWith("feature/user-auth-2.0")
|
||||
|
||||
// Should create collection with sanitized branch name
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456-br-feature-user-auth-2-0",
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("should return the current branch name when branch isolation is enabled", async () => {
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue("main")
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue("main")
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
expect(vectorStore.getCurrentBranch()).toBe("main")
|
||||
})
|
||||
|
||||
it("should handle very long branch names", async () => {
|
||||
const longBranchName = "feature/" + "a".repeat(100)
|
||||
const sanitizedLongName = "feature-" + "a".repeat(42) // Truncated to 50 chars
|
||||
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue(longBranchName)
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue(sanitizedLongName)
|
||||
|
||||
const vectorStore = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
// Mock collection doesn't exist
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore.initialize()
|
||||
|
||||
// Should handle long branch name
|
||||
expect(gitUtils.sanitizeBranchName).toHaveBeenCalledWith(longBranchName)
|
||||
|
||||
// Should create collection with truncated branch name
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
`ws-mockedhash123456-br-${sanitizedLongName}`,
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Branch Switching", () => {
|
||||
it("should use different collection names for different branches", async () => {
|
||||
// First initialization on main branch
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue("main")
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue("main")
|
||||
|
||||
const vectorStore1 = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore1.initialize()
|
||||
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456-br-main",
|
||||
expect.any(Object),
|
||||
)
|
||||
|
||||
// Clear mocks for second initialization
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Second initialization on feature branch
|
||||
;(gitUtils.getCurrentBranch as any).mockResolvedValue("feature/new-feature")
|
||||
;(gitUtils.sanitizeBranchName as any).mockReturnValue("feature-new-feature")
|
||||
|
||||
const vectorStore2 = new QdrantVectorStore(
|
||||
mockWorkspacePath,
|
||||
mockQdrantUrl,
|
||||
mockVectorSize,
|
||||
mockApiKey,
|
||||
true,
|
||||
)
|
||||
|
||||
mockQdrantClientInstance.getCollection.mockRejectedValue({
|
||||
status: 404,
|
||||
message: "Collection not found",
|
||||
})
|
||||
|
||||
await vectorStore2.initialize()
|
||||
|
||||
expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledWith(
|
||||
"ws-mockedhash123456-br-feature-new-feature",
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -6,6 +6,7 @@ import { IVectorStore } from "../interfaces/vector-store"
|
|||
import { Payload, VectorStoreSearchResult } from "../interfaces"
|
||||
import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../constants"
|
||||
import { t } from "../../../i18n"
|
||||
import { getCurrentBranch, sanitizeBranchName } from "../../../utils/git"
|
||||
|
||||
/**
|
||||
* Qdrant implementation of the vector store interface
|
||||
|
|
@ -18,19 +19,31 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
private readonly collectionName: string
|
||||
private readonly qdrantUrl: string = "http://localhost:6333"
|
||||
private readonly workspacePath: string
|
||||
private readonly branchIsolationEnabled: boolean
|
||||
private currentBranch: string | null = null
|
||||
|
||||
/**
|
||||
* Creates a new Qdrant vector store
|
||||
* @param workspacePath Path to the workspace
|
||||
* @param url Optional URL to the Qdrant server
|
||||
* @param vectorSize Size of the vector embeddings
|
||||
* @param apiKey Optional API key for Qdrant
|
||||
* @param branchIsolationEnabled Whether to use branch-specific collections
|
||||
*/
|
||||
constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) {
|
||||
constructor(
|
||||
workspacePath: string,
|
||||
url: string,
|
||||
vectorSize: number,
|
||||
apiKey?: string,
|
||||
branchIsolationEnabled: boolean = false,
|
||||
) {
|
||||
// Parse the URL to determine the appropriate QdrantClient configuration
|
||||
const parsedUrl = this.parseQdrantUrl(url)
|
||||
|
||||
// Store the resolved URL for our property
|
||||
this.qdrantUrl = parsedUrl
|
||||
this.workspacePath = workspacePath
|
||||
this.branchIsolationEnabled = branchIsolationEnabled
|
||||
|
||||
try {
|
||||
const urlObj = new URL(parsedUrl)
|
||||
|
|
@ -80,7 +93,18 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
// Generate collection name from workspace path
|
||||
const hash = createHash("sha256").update(workspacePath).digest("hex")
|
||||
this.vectorSize = vectorSize
|
||||
this.collectionName = `ws-${hash.substring(0, 16)}`
|
||||
|
||||
// Base collection name
|
||||
let collectionName = `ws-${hash.substring(0, 16)}`
|
||||
|
||||
// Add branch suffix if branch isolation is enabled
|
||||
if (this.branchIsolationEnabled) {
|
||||
// We'll set the actual collection name in initialize() after detecting the branch
|
||||
// For now, just store the base name
|
||||
collectionName = `ws-${hash.substring(0, 16)}`
|
||||
}
|
||||
|
||||
this.collectionName = collectionName
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -147,6 +171,11 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
* @returns Promise resolving to boolean indicating if a new collection was created
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
// Update collection name based on current branch if branch isolation is enabled
|
||||
if (this.branchIsolationEnabled) {
|
||||
await this.updateCollectionNameForBranch()
|
||||
}
|
||||
|
||||
let created = false
|
||||
try {
|
||||
const collectionInfo = await this.getCollectionInfo()
|
||||
|
|
@ -548,4 +577,37 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
const collectionInfo = await this.getCollectionInfo()
|
||||
return collectionInfo !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the collection name based on the current Git branch
|
||||
* Only called when branch isolation is enabled
|
||||
*/
|
||||
private async updateCollectionNameForBranch(): Promise<void> {
|
||||
const branch = await getCurrentBranch(this.workspacePath)
|
||||
|
||||
// Generate base collection name
|
||||
const hash = createHash("sha256").update(this.workspacePath).digest("hex")
|
||||
let collectionName = `ws-${hash.substring(0, 16)}`
|
||||
|
||||
if (branch) {
|
||||
// Sanitize branch name for use in collection name
|
||||
const sanitizedBranch = sanitizeBranchName(branch)
|
||||
collectionName = `${collectionName}-br-${sanitizedBranch}`
|
||||
this.currentBranch = branch
|
||||
} else {
|
||||
// Detached HEAD or not a git repo - use workspace-only collection
|
||||
this.currentBranch = null
|
||||
}
|
||||
|
||||
// Update the collection name
|
||||
;(this as any).collectionName = collectionName
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current branch being used for the collection
|
||||
* @returns The current branch name or null if not using branch isolation
|
||||
*/
|
||||
public getCurrentBranch(): string | null {
|
||||
return this.branchIsolationEnabled ? this.currentBranch : null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,3 +355,67 @@ export async function getWorkingState(cwd: string): Promise<string> {
|
|||
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current Git branch name for a given directory
|
||||
* @param cwd The directory to check (defaults to current working directory)
|
||||
* @returns The current branch name, or null if not in a git repo or in detached HEAD state
|
||||
*/
|
||||
export async function getCurrentBranch(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Try to get the current branch name
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd })
|
||||
const branch = stdout.trim()
|
||||
|
||||
// Check if we're in detached HEAD state
|
||||
if (branch === "HEAD") {
|
||||
return null
|
||||
}
|
||||
|
||||
return branch
|
||||
} catch (error) {
|
||||
console.error("Error getting current branch:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a branch name for use in collection names
|
||||
* @param branchName The original branch name
|
||||
* @returns Sanitized branch name suitable for use in identifiers
|
||||
*/
|
||||
export function sanitizeBranchName(branchName: string): string {
|
||||
// Convert to lowercase
|
||||
let sanitized = branchName.toLowerCase()
|
||||
|
||||
// Replace non-alphanumeric characters with hyphens
|
||||
sanitized = sanitized.replace(/[^a-z0-9]+/g, "-")
|
||||
|
||||
// Remove leading/trailing hyphens
|
||||
sanitized = sanitized.replace(/^-+|-+$/g, "")
|
||||
|
||||
// Collapse multiple consecutive hyphens
|
||||
sanitized = sanitized.replace(/-+/g, "-")
|
||||
|
||||
// Limit length to 50 characters (reasonable limit for branch segment)
|
||||
if (sanitized.length > 50) {
|
||||
sanitized = sanitized.substring(0, 50).replace(/-+$/, "")
|
||||
}
|
||||
|
||||
// If empty after sanitization, use a default
|
||||
if (!sanitized) {
|
||||
sanitized = "default"
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface CodeIndexPopoverProps {
|
|||
interface LocalCodeIndexSettings {
|
||||
// Global state settings
|
||||
codebaseIndexEnabled: boolean
|
||||
codebaseIndexBranchIsolation: boolean
|
||||
codebaseIndexQdrantUrl: string
|
||||
codebaseIndexEmbedderProvider: EmbedderProvider
|
||||
codebaseIndexEmbedderBaseUrl?: string
|
||||
|
|
@ -180,6 +181,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
// Default settings template
|
||||
const getDefaultSettings = (): LocalCodeIndexSettings => ({
|
||||
codebaseIndexEnabled: true,
|
||||
codebaseIndexBranchIsolation: false,
|
||||
codebaseIndexQdrantUrl: "",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
codebaseIndexEmbedderBaseUrl: "",
|
||||
|
|
@ -212,6 +214,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
if (codebaseIndexConfig) {
|
||||
const settings = {
|
||||
codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true,
|
||||
codebaseIndexBranchIsolation: codebaseIndexConfig.codebaseIndexBranchIsolation ?? false,
|
||||
codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "",
|
||||
codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai",
|
||||
codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
|
||||
|
|
@ -508,8 +511,9 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
settingsToSave[key] = value
|
||||
}
|
||||
|
||||
// Always include codebaseIndexEnabled to ensure it's persisted
|
||||
// Always include codebaseIndexEnabled and codebaseIndexBranchIsolation to ensure they're persisted
|
||||
settingsToSave.codebaseIndexEnabled = currentSettings.codebaseIndexEnabled
|
||||
settingsToSave.codebaseIndexBranchIsolation = currentSettings.codebaseIndexBranchIsolation
|
||||
|
||||
// Save settings to backend
|
||||
vscode.postMessage({
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@
|
|||
"qdrantApiKeyPlaceholder": "Enter your Qdrant API key (optional)",
|
||||
"setupConfigLabel": "Setup",
|
||||
"advancedConfigLabel": "Advanced Configuration",
|
||||
"advancedConfiguration": "Advanced Configuration",
|
||||
"branchIsolation": {
|
||||
"enableLabel": "Enable Branch Isolation",
|
||||
"enableDescription": "When enabled, each Git branch will have its own separate index. This ensures search results always reflect the current branch's code.",
|
||||
"storageWarning": "Warning: Enabling branch isolation will increase storage usage as each branch maintains its own index."
|
||||
},
|
||||
"searchMinScoreLabel": "Search Score Threshold",
|
||||
"searchMinScoreDescription": "Minimum similarity score (0.0-1.0) required for search results. Lower values return more results but may be less relevant. Higher values return fewer but more relevant results.",
|
||||
"searchMinScoreResetTooltip": "Reset to default value (0.4)",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue