[tab] src/gitProvider.ts (typescript)
/**
 * Git Provider for crowd-code 2.0
 * Enhanced git operation detection including stash, pull, checkout, merge
 */

import * as vscode from 'vscode'
import * as child_process from 'child_process'
import * as util from 'util'
import { logToOutput } from './utilities'
import { refreshCache } from './capture'
import type { GitAction } from './types'

// ============================================================================
// Types
// ============================================================================

type GitOperation = 'checkout' | 'stash' | 'pull' | 'merge' | 'other'

interface GitState {
	branch: string
	repository: string
	lastCommit: string | null
}

// ============================================================================
// State
// ============================================================================

let currentGitState: GitState | null = null
let gitWatcherInitialized = false
let gitStateCheckInterval: NodeJS.Timeout | undefined

// File system watchers for various git files
let gitHeadWatcher: vscode.FileSystemWatcher | undefined
let gitIndexWatcher: vscode.FileSystemWatcher | undefined
let gitStashWatcher: vscode.FileSystemWatcher | undefined
let gitRefsWatcher: vscode.FileSystemWatcher | undefined

// Callback for git actions
let onGitActionCallback: ((action: GitAction) => void) | null = null

// Track recent terminal commands to correlate with git file changes
const recentGitCommands: { command: string; timestamp: number }[] = []
const GIT_COMMAND_CORRELATION_WINDOW_MS = 5000

// ============================================================================
// Callback Management
// ============================================================================

/**
 * Set the callback for git action events
 */
export function setGitActionCallback(callback: ((action: GitAction) => void) | null): void {
	onGitActionCallback = callback
}

// ============================================================================
// Git Command Detection from Terminal
// ============================================================================

/**
 * Check if a terminal command is a git operation and record it
 */
export function checkTerminalForGitCommand(command: string): void {
	const gitCommandPatterns = [
		{ pattern: /^git\s+checkout\b/, operation: 'checkout' as GitOperation },
		{ pattern: /^git\s+switch\b/, operation: 'checkout' as GitOperation },
		{ pattern: /^git\s+stash\b/, operation: 'stash' as GitOperation },
		{ pattern: /^git\s+pull\b/, operation: 'pull' as GitOperation },
		{ pattern: /^git\s+merge\b/, operation: 'merge' as GitOperation },
		{ pattern: /^git\s+rebase\b/, operation: 'merge' as GitOperation },
		{ pattern: /^git\s+reset\b/, operation: 'other' as GitOperation },
		{ pattern: /^git\s+revert\b/, operation: 'other' as GitOperation },
		{ pattern: /^git\s+cherry-pick\b/, operation: 'other' as GitOperation },
	]

	const trimmedCommand = command.trim()
	
	for (const { pattern, operation } of gitCommandPatterns) {
		if (pattern.test(trimmedCommand)) {
			recentGitCommands.push({
				command: trimmedCommand,
				timestamp: Date.now(),
			})

			// Keep only recent commands
			const cutoff = Date.now() - GIT_COMMAND_CORRELATION_WINDOW_MS
			while (recentGitCommands.length > 0 && recentGitCommands[0].timestamp < cutoff) {
				recentGitCommands.shift()
			}

			logToOutput(`Detected git command: ${trimmedCommand} (${operation})`, 'info')
			break
		}
	}
}

/**
 * Get the most recent git command within the correlation window
 */
function getRecentGitCommand(): { command: string; operation: GitOperation } | null {
	const now = Date.now()
	
	for (let i = recentGitCommands.length - 1; i >= 0; i--) {
		const entry = recentGitCommands[i]
		if (now - entry.timestamp < GIT_COMMAND_CORRELATION_WINDOW_MS) {
			// Determine operation type
			const command = entry.command
			if (/checkout|switch/.test(command)) return { command, operation: 'checkout' }
			if (/stash/.test(command)) return { command, operation: 'stash' }
			if (/pull/.test(command)) return { command, operation: 'pull' }
			if (/merge|rebase/.test(command)) return { command, operation: 'merge' }
			return { command, operation: 'other' }
		}
	}
	
	return null
}

// ============================================================================
// Git State Detection
// ============================================================================

const execAsync = util.promisify(child_process.exec)

async function getWorkspacePath(): Promise<string | null> {
	const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
	return workspaceFolder?.uri.fsPath || null
}

async function getCurrentBranch(): Promise<string | null> {
	try {
		const cwd = await getWorkspacePath()
		if (!cwd) return null
		
		const { stdout } = await execAsync('git branch --show-current', { cwd })
		return stdout.trim() || null
	} catch {
		return null
	}
}

async function getCurrentCommit(): Promise<string | null> {
	try {
		const cwd = await getWorkspacePath()
		if (!cwd) return null
		
		const { stdout } = await execAsync('git rev-parse HEAD', { cwd })
		return stdout.trim() || null
	} catch {
		return null
	}
}

// ============================================================================
// Git Action Handling
// ============================================================================

function emitGitAction(operation: GitOperation, details: string): void {
	if (!onGitActionCallback) return

	const action: GitAction = {
		kind: 'git',
		source: 'external',
		operation,
		details,
	}

	onGitActionCallback(action)
	logToOutput(`Git action emitted: ${operation} - ${details}`, 'info')
}

async function handleGitFileChange(fileType: 'head' | 'index' | 'stash' | 'refs'): Promise<void> {
	logToOutput(`Git ${fileType} file changed`, 'info')

	// Check if there's a recent git command to correlate with
	const recentCommand = getRecentGitCommand()
	
	// Detect what changed
	const newBranch = await getCurrentBranch()
	const newCommit = await getCurrentCommit()
	const workspacePath = await getWorkspacePath()
	const repository = workspacePath || 'unknown'

	let operation: GitOperation = 'other'
	let details = ''

	if (fileType === 'head') {
		// HEAD changed - likely checkout or commit
		if (currentGitState && newBranch !== currentGitState.branch) {
			operation = 'checkout'
			details = `Branch changed from '${currentGitState.branch}' to '${newBranch}'`
		} else if (currentGitState && newCommit !== currentGitState.lastCommit) {
			// Commit changed but branch didn't - could be pull, merge, etc.
			if (recentCommand) {
				operation = recentCommand.operation
				details = recentCommand.command
			} else {
				operation = 'other'
				details = `HEAD moved to ${newCommit?.substring(0, 7)}`
			}
		}
	} else if (fileType === 'stash') {
		operation = 'stash'
		details = recentCommand?.command || 'Stash operation detected'
	} else if (fileType === 'refs') {
		// refs changed - could be pull, push, fetch, etc.
		if (recentCommand) {
			operation = recentCommand.operation
			details = recentCommand.command
		} else {
			operation = 'other'
			details = 'Git refs changed'
		}
	} else if (fileType === 'index') {
		// Index changed - staging area modified
		// This is usually not a major operation we need to track
		return
	}

	// Update state
	if (newBranch) {
		currentGitState = {
			branch: newBranch,
			repository,
			lastCommit: newCommit,
		}
	}

	// Emit action if something significant happened
	if (details) {
		emitGitAction(operation, details)
		
		// Refresh file cache after git operations
		logToOutput('Refreshing file cache due to git operation', 'info')
		await refreshCache()
	}
}

// ============================================================================
// Initialization
// ============================================================================

async function tryInitializeGitProvider(): Promise<void> {
	try {
		const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
		if (!workspaceFolder) {
			logToOutput('No workspace folder found', 'info')
			return
		}

		// Check if this is a git repository
		const gitDir = vscode.Uri.joinPath(workspaceFolder.uri, '.git')
		try {
			await vscode.workspace.fs.stat(gitDir)
			logToOutput('Git repository found', 'info')

			// Get initial state
			const branch = await getCurrentBranch()
			const commit = await getCurrentCommit()
			
			currentGitState = {
				branch: branch || 'unknown',
				repository: workspaceFolder.uri.fsPath,
				lastCommit: commit,
			}
			
			logToOutput(`Initial git state: branch=${branch}, commit=${commit?.substring(0, 7)}`, 'info')

			// Watch .git/HEAD for branch changes
			gitHeadWatcher = vscode.workspace.createFileSystemWatcher(
				new vscode.RelativePattern(workspaceFolder, '.git/HEAD')
			)
			gitHeadWatcher.onDidChange(() => handleGitFileChange('head'))

			// Watch .git/index for staging changes (less important)
			gitIndexWatcher = vscode.workspace.createFileSystemWatcher(
				new vscode.RelativePattern(workspaceFolder, '.git/index')
			)
			gitIndexWatcher.onDidChange(() => handleGitFileChange('index'))

			// Watch .git/refs for reference changes (pull, push, etc.)
			gitRefsWatcher = vscode.workspace.createFileSystemWatcher(
				new vscode.RelativePattern(workspaceFolder, '.git/refs/**/*')
			)
			gitRefsWatcher.onDidChange(() => handleGitFileChange('refs'))
			gitRefsWatcher.onDidCreate(() => handleGitFileChange('refs'))
			gitRefsWatcher.onDidDelete(() => handleGitFileChange('refs'))

			// Watch .git/refs/stash for stash operations
			gitStashWatcher = vscode.workspace.createFileSystemWatcher(
				new vscode.RelativePattern(workspaceFolder, '.git/refs/stash')
			)
			gitStashWatcher.onDidChange(() => handleGitFileChange('stash'))
			gitStashWatcher.onDidCreate(() => handleGitFileChange('stash'))
			gitStashWatcher.onDidDelete(() => handleGitFileChange('stash'))

			// Periodic state check as fallback
			gitStateCheckInterval = setInterval(async () => {
				const newBranch = await getCurrentBranch()
				const newCommit = await getCurrentCommit()
				
				if (currentGitState) {
					if (newBranch && newBranch !== currentGitState.branch) {
						logToOutput(`Periodic check detected branch change: ${currentGitState.branch} -> ${newBranch}`, 'info')
						await handleGitFileChange('head')
					} else if (newCommit && newCommit !== currentGitState.lastCommit) {
						logToOutput(`Periodic check detected commit change: ${currentGitState.lastCommit?.substring(0, 7)} -> ${newCommit.substring(0, 7)}`, 'info')
						await handleGitFileChange('head')
					}
				}
			}, 5000)

			gitWatcherInitialized = true
			logToOutput('Git provider 2.0 initialized successfully', 'info')
		} catch {
			logToOutput('Not a git repository', 'info')
		}
	} catch (error) {
		logToOutput(`Error initializing git provider: ${error}`, 'error')
	}
}

/**
 * Initialize the git provider
 */
export function initializeGitProvider(context: vscode.ExtensionContext): void {
	logToOutput('Initializing git provider 2.0...', 'info')

	// Try to initialize immediately
	tryInitializeGitProvider().catch(error => {
		logToOutput(`Error in initial git provider initialization: ${error}`, 'error')
	})

	// Retry after a delay
	setTimeout(() => {
		if (!gitWatcherInitialized) {
			logToOutput('Retrying git provider initialization...', 'info')
			tryInitializeGitProvider().catch(error => {
				logToOutput(`Error in retry git provider initialization: ${error}`, 'error')
			})
		}
	}, 2000)

	// Listen for workspace changes
	context.subscriptions.push(
		vscode.workspace.onDidChangeWorkspaceFolders(() => {
			if (!gitWatcherInitialized) {
				logToOutput('Workspace folders changed, retrying git provider initialization...', 'info')
				tryInitializeGitProvider().catch(error => {
					logToOutput(`Error in workspace change git provider initialization: ${error}`, 'error')
				})
			}
		})
	)
}

/**
 * Cleanup the git provider
 */
export function cleanupGitProvider(): void {
	if (gitStateCheckInterval) {
		clearInterval(gitStateCheckInterval)
		gitStateCheckInterval = undefined
	}

	if (gitHeadWatcher) {
		gitHeadWatcher.dispose()
		gitHeadWatcher = undefined
	}

	if (gitIndexWatcher) {
		gitIndexWatcher.dispose()
		gitIndexWatcher = undefined
	}

	if (gitStashWatcher) {
		gitStashWatcher.dispose()
		gitStashWatcher = undefined
	}

	if (gitRefsWatcher) {
		gitRefsWatcher.dispose()
		gitRefsWatcher = undefined
	}

	currentGitState = null
	gitWatcherInitialized = false
	recentGitCommands.length = 0
}

/**
 * Get the current git state (for debugging/testing)
 */
export function getCurrentGitState(): GitState | null {
	return currentGitState
}