Skip to content

Commit 3bdfd5b

Browse files
committed
Adds Git branch monitoring
Introduces a new GitBranchWatcher for improved branch isolation: Extracts branch monitoring logic into a separate reusable class Optimizes cache management to avoid redundant API calls Improves error handling and resource cleanup Enables more efficient branch switching with intelligent reindexing This change simplifies maintenance and increases reliability of branch-related functionality.
1 parent 8ed1c89 commit 3bdfd5b

File tree

3 files changed

+239
-65
lines changed

3 files changed

+239
-65
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as vscode from "vscode"
2+
import { getCurrentBranch } from "../../utils/git"
3+
4+
/**
5+
* Callback function type for branch change events
6+
*/
7+
export type BranchChangeCallback = (oldBranch: string | undefined, newBranch: string | undefined) => Promise<void>
8+
9+
/**
10+
* Configuration options for GitBranchWatcher
11+
*/
12+
export interface GitBranchWatcherConfig {
13+
/** Debounce delay in milliseconds (default: 500ms) */
14+
debounceMs?: number
15+
/** Whether the watcher is enabled */
16+
enabled: boolean
17+
}
18+
19+
/**
20+
* Watches for Git branch changes in a workspace and notifies listeners.
21+
*
22+
* Responsibilities:
23+
* - Monitor .git/HEAD file for changes
24+
* - Detect branch switches
25+
* - Debounce rapid changes
26+
* - Cache current branch to avoid redundant I/O
27+
* - Notify listeners of branch changes
28+
*/
29+
export class GitBranchWatcher implements vscode.Disposable {
30+
private _watcher?: vscode.FileSystemWatcher
31+
private _currentBranch?: string
32+
private _debounceTimer?: ReturnType<typeof setTimeout>
33+
private _callback: BranchChangeCallback
34+
private _config: GitBranchWatcherConfig
35+
private readonly _workspacePath: string
36+
37+
/**
38+
* Creates a new GitBranchWatcher
39+
* @param workspacePath Path to the workspace to watch
40+
* @param callback Function to call when branch changes
41+
* @param config Configuration options
42+
*/
43+
constructor(workspacePath: string, callback: BranchChangeCallback, config: GitBranchWatcherConfig) {
44+
this._workspacePath = workspacePath
45+
this._callback = callback
46+
this._config = config
47+
}
48+
49+
/**
50+
* Initializes the watcher and starts monitoring for branch changes
51+
*/
52+
async initialize(): Promise<void> {
53+
if (!this._config.enabled) {
54+
this.dispose()
55+
return
56+
}
57+
58+
// Cache initial branch to avoid redundant I/O
59+
this._currentBranch = await getCurrentBranch(this._workspacePath)
60+
61+
// Only create watcher if it doesn't exist
62+
if (!this._watcher) {
63+
const pattern = new vscode.RelativePattern(this._workspacePath, ".git/HEAD")
64+
this._watcher = vscode.workspace.createFileSystemWatcher(pattern)
65+
66+
const handler = () => this._onGitHeadChange()
67+
this._watcher.onDidChange(handler)
68+
this._watcher.onDidCreate(handler)
69+
this._watcher.onDidDelete(handler)
70+
}
71+
}
72+
73+
/**
74+
* Updates the watcher configuration
75+
* @param config New configuration
76+
*/
77+
async updateConfig(config: GitBranchWatcherConfig): Promise<void> {
78+
this._config = config
79+
await this.initialize()
80+
}
81+
82+
/**
83+
* Gets the currently cached branch name
84+
* @returns Current branch name or undefined if not in a git repo
85+
*/
86+
getCurrentBranch(): string | undefined {
87+
return this._currentBranch
88+
}
89+
90+
/**
91+
* Handles .git/HEAD file changes with debouncing
92+
*/
93+
private _onGitHeadChange(): void {
94+
// Clear existing debounce timer
95+
if (this._debounceTimer) {
96+
clearTimeout(this._debounceTimer)
97+
}
98+
99+
// Debounce to handle rapid branch switches
100+
const debounceMs = this._config.debounceMs ?? 500
101+
this._debounceTimer = setTimeout(async () => {
102+
try {
103+
if (!this._config.enabled) return
104+
105+
// Detect branch change
106+
const oldBranch = this._currentBranch
107+
const newBranch = await getCurrentBranch(this._workspacePath)
108+
109+
// Only notify if branch actually changed
110+
if (newBranch !== oldBranch) {
111+
// Update cached branch BEFORE calling callback
112+
// This ensures getCurrentBranch() returns the new branch immediately
113+
this._currentBranch = newBranch
114+
115+
// Notify listener
116+
await this._callback(oldBranch, newBranch)
117+
}
118+
} catch (error) {
119+
console.error("[GitBranchWatcher] Failed to handle branch change:", error)
120+
}
121+
}, debounceMs)
122+
}
123+
124+
/**
125+
* Disposes the watcher and cleans up resources
126+
*/
127+
dispose(): void {
128+
if (this._debounceTimer) {
129+
clearTimeout(this._debounceTimer)
130+
this._debounceTimer = undefined
131+
}
132+
133+
if (this._watcher) {
134+
this._watcher.dispose()
135+
this._watcher = undefined
136+
}
137+
138+
this._currentBranch = undefined
139+
}
140+
}

src/services/code-index/manager.ts

Lines changed: 53 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CodeIndexServiceFactory } from "./service-factory"
88
import { CodeIndexSearchService } from "./search-service"
99
import { CodeIndexOrchestrator } from "./orchestrator"
1010
import { CacheManager } from "./cache-manager"
11+
import { GitBranchWatcher } from "./git-branch-watcher"
1112
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
1213
import fs from "fs/promises"
1314
import ignore from "ignore"
@@ -16,8 +17,6 @@ import { t } from "../../i18n"
1617
import { TelemetryService } from "@roo-code/telemetry"
1718
import { TelemetryEventName } from "@roo-code/types"
1819

19-
import { getCurrentBranch } from "../../utils/git"
20-
2120
export class CodeIndexManager {
2221
// --- Singleton Implementation ---
2322
private static instances = new Map<string, CodeIndexManager>() // Map workspace path to instance
@@ -31,13 +30,10 @@ export class CodeIndexManager {
3130
private _cacheManager: CacheManager | undefined
3231

3332
// Flag to prevent race conditions during error recovery
33+
private _isRecoveringFromError = false
3434

3535
// Git branch change watcher for branch isolation
36-
private _gitHeadWatcher?: vscode.FileSystemWatcher
37-
private _lastKnownBranch?: string
38-
private _gitHeadDebounce?: ReturnType<typeof setTimeout>
39-
40-
private _isRecoveringFromError = false
36+
private _gitBranchWatcher?: GitBranchWatcher
4137

4238
public static getInstance(context: vscode.ExtensionContext, workspacePath?: string): CodeIndexManager | undefined {
4339
// If workspacePath is not provided, try to get it from the active editor or first workspace folder
@@ -247,9 +243,9 @@ export class CodeIndexManager {
247243
// This ensures a clean slate even if state update failed
248244
this._configManager = undefined
249245
this._serviceFactory = undefined
250-
if (this._gitHeadWatcher) {
251-
this._gitHeadWatcher.dispose()
252-
this._gitHeadWatcher = undefined
246+
if (this._gitBranchWatcher) {
247+
this._gitBranchWatcher.dispose()
248+
this._gitBranchWatcher = undefined
253249
}
254250

255251
this._orchestrator = undefined
@@ -267,9 +263,9 @@ export class CodeIndexManager {
267263
if (this._orchestrator) {
268264
this.stopWatcher()
269265
}
270-
if (this._gitHeadWatcher) {
271-
this._gitHeadWatcher.dispose()
272-
this._gitHeadWatcher = undefined
266+
if (this._gitBranchWatcher) {
267+
this._gitBranchWatcher.dispose()
268+
this._gitBranchWatcher = undefined
273269
}
274270

275271
this._stateManager.dispose()
@@ -399,59 +395,55 @@ export class CodeIndexManager {
399395

400396
// --- Git branch watcher (Phase 1: auto branch switch handling) ---
401397
private async _setupGitHeadWatcher(): Promise<void> {
402-
const isEnabled = this._configManager?.getConfig().branchIsolationEnabled
403-
if (!isEnabled) {
404-
if (this._gitHeadWatcher) {
405-
this._gitHeadWatcher.dispose()
406-
this._gitHeadWatcher = undefined
407-
}
408-
this._lastKnownBranch = undefined
409-
return
410-
}
411-
this._lastKnownBranch = await getCurrentBranch(this.workspacePath)
412-
if (!this._gitHeadWatcher) {
413-
const pattern = new vscode.RelativePattern(this.workspacePath, ".git/HEAD")
414-
this._gitHeadWatcher = vscode.workspace.createFileSystemWatcher(pattern)
415-
const handler = () => this._onGitHeadChange()
416-
this._gitHeadWatcher.onDidChange(handler)
417-
418-
this._gitHeadWatcher.onDidCreate(handler)
419-
this._gitHeadWatcher.onDidDelete(handler)
398+
const isEnabled = this._configManager?.getConfig().branchIsolationEnabled ?? false
399+
400+
// Create watcher if it doesn't exist
401+
if (!this._gitBranchWatcher) {
402+
this._gitBranchWatcher = new GitBranchWatcher(
403+
this.workspacePath,
404+
async (oldBranch, newBranch) => this._onBranchChange(oldBranch, newBranch),
405+
{ enabled: isEnabled, debounceMs: 500 },
406+
)
407+
} else {
408+
// Update existing watcher config
409+
await this._gitBranchWatcher.updateConfig({ enabled: isEnabled, debounceMs: 500 })
420410
}
411+
412+
// Initialize the watcher
413+
await this._gitBranchWatcher.initialize()
421414
}
422415

423-
private async _onGitHeadChange(): Promise<void> {
424-
if (this._gitHeadDebounce) clearTimeout(this._gitHeadDebounce)
425-
this._gitHeadDebounce = setTimeout(async () => {
426-
try {
427-
if (!this._configManager?.getConfig().branchIsolationEnabled) return
428-
const newBranch = await getCurrentBranch(this.workspacePath)
429-
if (newBranch === this._lastKnownBranch) return
430-
this._lastKnownBranch = newBranch
431-
await this._recreateServices()
432-
433-
// Smart re-indexing: only do full scan if collection doesn't exist or is empty
434-
// If collection exists with data, file watcher will handle incremental updates
435-
const vectorStore = this._orchestrator?.getVectorStore()
436-
if (!vectorStore) {
437-
// No orchestrator yet, just start indexing
438-
this._orchestrator?.startIndexing()
439-
return
440-
}
416+
/**
417+
* Handles Git branch changes
418+
* @param oldBranch Previous branch name
419+
* @param newBranch New branch name
420+
*/
421+
private async _onBranchChange(oldBranch: string | undefined, newBranch: string | undefined): Promise<void> {
422+
try {
423+
// Recreate services with new branch context
424+
await this._recreateServices()
441425

442-
const collectionExists = await vectorStore.collectionExists()
443-
if (!collectionExists) {
444-
// New branch or first time indexing this branch - do full scan
445-
this._orchestrator?.startIndexing()
446-
} else {
447-
// Collection exists - just validate/initialize without full scan
448-
// File watcher will detect any file changes from the branch switch
449-
await vectorStore.initialize()
450-
}
451-
} catch (error) {
452-
console.error("Failed to handle Git branch change:", error)
426+
// Smart re-indexing: only do full scan if collection doesn't exist or is empty
427+
// If collection exists with data, file watcher will handle incremental updates
428+
const vectorStore = this._orchestrator?.getVectorStore()
429+
if (!vectorStore) {
430+
// No orchestrator yet, just start indexing
431+
this._orchestrator?.startIndexing()
432+
return
453433
}
454-
}, 250)
434+
435+
const collectionExists = await vectorStore.collectionExists()
436+
if (!collectionExists) {
437+
// New branch or first time indexing this branch - do full scan
438+
this._orchestrator?.startIndexing()
439+
} else {
440+
// Collection exists - just validate/initialize without full scan
441+
// File watcher will detect any file changes from the branch switch
442+
await vectorStore.initialize()
443+
}
444+
} catch (error) {
445+
console.error("[CodeIndexManager] Failed to handle Git branch change:", error)
446+
}
455447
}
456448

457449
/**

0 commit comments

Comments
 (0)