Git Worktree Implementation Specification

This document describes the technical implementation of Git worktree support in VibeTunnel.

Architecture Overview

VibeTunnel’s worktree support is built on three main components:
  1. Backend API - Git operations and worktree management
  2. Frontend UI - Session creation and worktree visualization
  3. Git Hooks - Automatic synchronization and follow mode

Backend Implementation

Core Services

GitService (web/src/server/services/git-service.ts)
  • Not implemented as a service, Git operations are embedded in routes
  • Client-side GitService exists at web/src/client/services/git-service.ts
Worktree Routes (web/src/server/routes/worktrees.ts)
  • GET /api/worktrees - List all worktrees with stats and follow mode status
  • POST /api/worktrees - Create new worktree
  • DELETE /api/worktrees/:branch - Remove worktree
  • POST /api/worktrees/switch - Switch branch and enable follow mode
  • POST /api/worktrees/follow - Enable/disable follow mode for a branch
Git Routes (web/src/server/routes/git.ts)
  • GET /api/git/repo-info - Get repository information
  • POST /api/git/event - Process git hook events (internal use)
  • GET /api/git/follow - Check follow mode status for a repository
  • GET /api/git/notifications - Get pending notifications

Key Functions

// List worktrees with extended information
async function listWorktreesWithStats(repoPath: string): Promise<Worktree[]>

// Create worktree with automatic path generation
async function createWorktree(
  repoPath: string, 
  branch: string, 
  path: string, 
  baseBranch?: string
): Promise<void>

// Handle branch switching with safety checks
async function switchBranch(
  repoPath: string, 
  branch: string
): Promise<void>

Git Operations

All Git operations use Node.js child_process.execFile for security:
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

// Execute git commands safely
async function execGit(args: string[], options?: { cwd?: string }) {
  return execFileAsync('git', args, {
    ...options,
    timeout: 30000,
    maxBuffer: 10 * 1024 * 1024, // 10MB
  });
}

Follow Mode Implementation

Follow mode uses Git hooks and git config for state management:
  1. State Storage: Git config vibetunnel.followWorktree
    # Follow mode stores the worktree path in main repository
    git config vibetunnel.followWorktree "/path/to/worktree"
    
    # Check follow mode status
    git config vibetunnel.followWorktree
    
    # Disable follow mode
    git config --unset vibetunnel.followWorktree
    
  2. Git Hooks: Installed in BOTH main repo and worktree
    • post-checkout: Detects branch switches
    • post-commit: Detects new commits
    • post-merge: Detects merge operations
  3. Event Processing: Hooks execute vt git event command
  4. Synchronization Logic:
    • Worktree events → Main repo syncs (branch, commits, checkouts)
    • Main repo commits → Worktree syncs (commits only)
    • Main repo branch switch → Auto-unfollow

Frontend Implementation

Components

SessionCreateForm (web/src/client/components/session-create-form.ts)
  • Branch/worktree selection UI
  • Smart branch switching logic
  • Warning displays for conflicts
WorktreeManager (web/src/client/components/worktree-manager.ts)
  • Dedicated worktree management UI
  • Follow mode controls
  • Worktree deletion and branch switching
  • Note: Does not include UI for creating new worktrees

State Management

// Session creation state
@state() private currentBranch: string = '';
@state() private selectedBaseBranch: string = '';
@state() private selectedWorktree?: string;
@state() private availableWorktrees: Worktree[] = [];

// Branch switching state  
@state() private branchSwitchWarning?: string;
@state() private isLoadingBranches = false;
@state() private isLoadingWorktrees = false;

Branch Selection Logic

The new session dialog implements smart branch handling:
  1. No Worktree Selected:
    if (selectedBaseBranch !== currentBranch) {
      try {
        await gitService.switchBranch(repoPath, selectedBaseBranch);
        effectiveBranch = selectedBaseBranch;
      } catch (error) {
        // Show warning, use current branch
        this.branchSwitchWarning = "Cannot switch due to uncommitted changes";
        effectiveBranch = currentBranch;
      }
    }
    
  2. Worktree Selected:
    // Use worktree's path and branch
    effectiveWorkingDir = worktreeInfo.path;
    effectiveBranch = selectedWorktree;
    // No branch switching occurs
    

UI Updates

Dynamic labels based on context:
${this.selectedWorktree ? 'Base Branch for Worktree:' : 'Switch to Branch:'}
Help text explaining behavior:
${this.selectedWorktree
  ? 'New worktree branch will be created from this branch'
  : this.selectedBaseBranch !== this.currentBranch
    ? `Session will start on ${this.selectedBaseBranch} (currently on ${this.currentBranch})`
    : `Current branch: ${this.currentBranch}`
}

Git Hook Integration

Hook Installation

Automatic hook installation on repository access:
// Install hooks when checking Git repository
async function installGitHooks(repoPath: string): Promise<void> {
  const hooks = ['post-commit', 'post-checkout'];
  for (const hook of hooks) {
    await installHook(repoPath, hook);
  }
}

Hook Script

The hook implementation uses the vt command:
#!/bin/sh
# VibeTunnel Git hook - post-checkout
# This hook notifies VibeTunnel when Git events occur

# Check if vt command is available
if command -v vt >/dev/null 2>&1; then
  # Run in background to avoid blocking Git operations
  vt git event &
fi

# Always exit successfully
exit 0
The vt git event command:
  • Sends the repository path to the server via POST /api/git/event
  • Server determines what changed by examining current git state
  • Triggers branch synchronization if follow mode is enabled
  • Sends notifications to connected sessions
  • Runs in background to avoid blocking git operations

Follow Mode Logic

The git event handler determines sync behavior based on event source:
// Get follow mode configuration
const followWorktree = await getGitConfig(mainRepoPath, 'vibetunnel.followWorktree');
if (!followWorktree) return; // Follow mode not enabled

// Determine if event is from main repo or worktree
const eventPath = req.body.repoPath;
const isFromWorktree = eventPath === followWorktree;
const isFromMain = eventPath === mainRepoPath;

if (isFromWorktree) {
  // Worktree → Main sync
  switch (event) {
    case 'checkout':
      // Sync branch or commit to main
      const target = req.body.branch || req.body.commit;
      await execGit(['checkout', target], { cwd: mainRepoPath });
      break;
    
    case 'commit':
    case 'merge':
      // Pull changes to main
      await execGit(['fetch'], { cwd: mainRepoPath });
      await execGit(['merge', 'FETCH_HEAD'], { cwd: mainRepoPath });
      break;
  }
} else if (isFromMain) {
  // Main → Worktree sync
  switch (event) {
    case 'checkout':
      // Branch switch in main = stop following
      await unsetGitConfig(mainRepoPath, 'vibetunnel.followWorktree');
      sendNotification('Follow mode disabled - switched branches in main repository');
      break;
    
    case 'commit':
      // Sync commit to worktree
      await execGit(['fetch'], { cwd: followWorktree });
      await execGit(['merge', 'FETCH_HEAD'], { cwd: followWorktree });
      break;
  }
}

Data Models

Worktree

The Worktree interface differs between backend and frontend: Backend (web/src/server/routes/worktrees.ts):
interface Worktree {
  path: string;
  branch: string;
  HEAD: string;
  detached: boolean;
  prunable?: boolean;
  locked?: boolean;
  lockedReason?: string;
  // Extended stats
  commitsAhead?: number;
  filesChanged?: number;
  insertions?: number;
  deletions?: number;
  hasUncommittedChanges?: boolean;
}
Frontend (web/src/client/services/git-service.ts):
interface Worktree extends BackendWorktree {
  // UI helpers - added dynamically by routes
  isMainWorktree?: boolean;
  isCurrentWorktree?: boolean;
}
The UI helper fields are computed dynamically in the worktree routes based on the current repository path and are not stored in the backend data model.

Session with Git Info

interface Session {
  id: string;
  name: string;
  command: string[];
  workingDir: string;
  // Git information (from shared/types.ts)
  gitRepoPath?: string;
  gitBranch?: string;
  gitAheadCount?: number;
  gitBehindCount?: number;
  gitHasChanges?: boolean;
  gitIsWorktree?: boolean;
  gitMainRepoPath?: string;
}

Error Handling

Common Errors

  1. Uncommitted Changes
    if (hasUncommittedChanges) {
      throw new Error('Cannot switch branches with uncommitted changes');
    }
    
  2. Branch Already Checked Out
    // Git automatically prevents this
    // Error: "fatal: 'branch' is already checked out at '/path/to/worktree'"
    
  3. Worktree Path Exists
    if (await pathExists(worktreePath)) {
      throw new Error(`Path already exists: ${worktreePath}`);
    }
    

Error Recovery

  • Show user-friendly warnings
  • Fallback to safe defaults
  • Never lose user work
  • Log detailed errors for debugging

Performance Considerations

Caching

  • Worktree list cached for 5 seconds
  • Branch list cached per repository
  • Git status cached with debouncing

Optimization

// Parallel operations where possible
const [branches, worktrees] = await Promise.all([
  loadBranches(repoPath),
  loadWorktrees(repoPath)
]);

// Debounced Git checks
this.gitCheckDebounceTimer = setTimeout(() => {
  this.checkGitRepository();
}, 500);

Security

Command Injection Prevention

All Git commands use array arguments:
// Safe
execFile('git', ['checkout', branchName])

// Never use string concatenation
// execFile('git checkout ' + branchName) // DANGEROUS

Path Validation

// Resolve and validate paths
const absolutePath = path.resolve(repoPath);
if (!absolutePath.startsWith(allowedBasePath)) {
  throw new Error('Invalid repository path');
}

Worktree Creation

Currently, worktree creation is handled through terminal commands rather than UI:
# Create a new worktree for an existing branch
git worktree add ../feature-branch feature-branch

# Create a new worktree with a new branch
git worktree add -b new-feature ../new-feature main

UI Support Status

  1. WorktreeManager (web/src/client/components/worktree-manager.ts)
    • No creation UI, only management of existing worktrees
    • Provides worktree switching, deletion, and follow mode controls
    • Shows worktree status (commits ahead, uncommitted changes)
  2. SessionCreateForm (web/src/client/components/session-create-form.ts)
    • Has worktree creation support through the git-branch-selector component
    • ✅ Creates worktrees and updates UI state properly
    • ✅ Selects newly created worktree after creation
    • ✅ Clears loading states and resets form on completion
    • ✅ Comprehensive branch name validation
    • ✅ Specific error messages for common failures
    • ⚠️ Uses simplistic path generation (repo path + branch slug)
    • ❌ No path customization UI
    • ❌ No option to create from specific base branch in UI
  3. Path Generation (web/src/client/components/session-create-form/git-utils.ts:100-103)
    • Simple approach: ${repoPath}-${branchSlug}
    • Branch names sanitized to alphanumeric + hyphens/underscores
    • No user customization of worktree location

Missing Features from Spec

  1. Worktree Path Customization
    • Current: Auto-generated paths only
    • Spec: Should allow custom path input
    • Impact: Users cannot organize worktrees in custom locations
  2. Base Branch Selection in UI
    • Current: Uses selected base branch from dropdown
    • Missing: No explicit UI to choose base branch during worktree creation
    • Workaround: Select base branch first, then create worktree
  3. Comprehensive E2E Tests
    • Unit tests exist: worktrees.test.ts, git-hooks.test.ts
    • Integration tests exist: worktree-workflows.test.ts
    • Missing: Full E2E tests for UI worktree creation flow

Testing

Unit Tests

  • worktrees.test.ts - Route handlers
  • git-hooks.test.ts - Hook installation
  • session-create-form.test.ts - UI logic

Integration Tests

  • worktree-workflows.test.ts - Full workflows
  • follow-mode.test.ts - Follow mode scenarios

E2E Tests

  • Create worktree via UI
  • Switch branches with warnings
  • Follow mode synchronization

Implementation Summary

✅ Fully Implemented

  1. Backend API - All planned endpoints functional
    • List, create, delete, switch, follow mode operations
    • Git hook integration for automatic branch following
    • Proper error handling and validation
  2. Follow Mode - Complete implementation
    • Git config storage (vibetunnel.followBranch)
    • Automatic branch synchronization via hooks
    • UI controls in WorktreeManager and SessionCreateForm
  3. Basic Worktree Creation - Functional with recent fixes
    • Create new worktrees from SessionCreateForm
    • Branch name validation
    • UI state management
    • Error handling with specific messages

⚠️ Partially Implemented

  1. Path Generation - Simplified version only
    • Auto-generates paths as ${repoPath}-${branchSlug}
    • No user customization option
    • Works for basic use cases
  2. Testing - Good coverage but missing E2E
    • Unit tests for routes and utilities
    • Integration tests for workflows
    • Missing: Full E2E tests with UI interactions

❌ Not Implemented

  1. Advanced Worktree Creation UI
    • Custom path input field
    • Path validation and suggestions
    • Preview of final worktree location
  2. WorktreeManager Creation UI
    • No worktree creation in management view
    • Must use SessionCreateForm or terminal
  3. Worktree Templates/Presets
    • No saved worktree configurations
    • No quick-create from templates