Skip to content

Git API

The Git API provides comprehensive functionality for Git version control integration within the development environment.

Overview

The Git API enables you to:

  • Access Git repository information
  • Perform Git operations (commit, push, pull, etc.)
  • Monitor repository status and changes
  • Manage branches and remotes
  • Handle merge conflicts
  • Integrate with Git workflows
  • Provide Git-related UI components
  • Support Git hooks and automation

Basic Usage

Repository Access

typescript
import { TraeAPI } from '@trae/api';

// Git repository manager
class GitManager {
  private repositories: Map<string, GitRepository> = new Map();
  private statusCache: Map<string, GitStatus> = new Map();
  private watchers: Map<string, TraeAPI.FileSystemWatcher> = new Map();
  private eventEmitter = new TraeAPI.EventEmitter<GitEvents>();

  constructor() {
    this.initializeRepositories();
    this.setupFileWatchers();
    this.setupEventListeners();
  }

  private async initializeRepositories(): Promise<void> {
    // Discover Git repositories in workspace
    const workspaceFolders = TraeAPI.workspace.workspaceFolders;
    if (!workspaceFolders) return;

    for (const folder of workspaceFolders) {
      await this.discoverRepository(folder.uri.fsPath);
    }
  }

  private async discoverRepository(path: string): Promise<GitRepository | null> {
    try {
      // Check if directory contains .git folder
      const gitPath = TraeAPI.path.join(path, '.git');
      const gitStat = await TraeAPI.workspace.fs.stat(TraeAPI.Uri.file(gitPath));
      
      if (gitStat.type === TraeAPI.FileType.Directory) {
        const repository = await this.createRepository(path);
        this.repositories.set(path, repository);
        
        console.log(`Discovered Git repository: ${path}`);
        this.eventEmitter.fire({ type: 'repositoryDiscovered', repository });
        
        return repository;
      }
    } catch (error) {
      // Not a Git repository or access error
    }
    
    return null;
  }

  private async createRepository(rootPath: string): Promise<GitRepository> {
    const repository: GitRepository = {
      rootPath,
      gitPath: TraeAPI.path.join(rootPath, '.git'),
      name: TraeAPI.path.basename(rootPath),
      status: await this.getRepositoryStatus(rootPath),
      branches: await this.getBranches(rootPath),
      remotes: await this.getRemotes(rootPath),
      head: await this.getHead(rootPath),
      config: await this.getConfig(rootPath),
      lastUpdated: Date.now()
    };

    return repository;
  }

  // Repository operations
  async getRepository(path: string): Promise<GitRepository | null> {
    // Check cache first
    let repository = this.repositories.get(path);
    if (repository) {
      return repository;
    }

    // Try to discover repository
    repository = await this.discoverRepository(path);
    return repository;
  }

  async getRepositoryForFile(filePath: string): Promise<GitRepository | null> {
    // Find repository that contains this file
    for (const [repoPath, repository] of this.repositories) {
      if (filePath.startsWith(repoPath)) {
        return repository;
      }
    }

    // Try to discover repository by walking up directory tree
    let currentPath = TraeAPI.path.dirname(filePath);
    while (currentPath !== TraeAPI.path.dirname(currentPath)) {
      const repository = await this.discoverRepository(currentPath);
      if (repository) {
        return repository;
      }
      currentPath = TraeAPI.path.dirname(currentPath);
    }

    return null;
  }

  getAllRepositories(): GitRepository[] {
    return Array.from(this.repositories.values());
  }

  // Status operations
  async getRepositoryStatus(repoPath: string): Promise<GitStatus> {
    try {
      // Check cache
      const cached = this.statusCache.get(repoPath);
      if (cached && Date.now() - cached.timestamp < 5000) {
        return cached;
      }

      // Execute git status command
      const result = await this.executeGitCommand(repoPath, ['status', '--porcelain=v1', '-b']);
      const status = this.parseGitStatus(result.stdout);
      
      // Cache result
      this.statusCache.set(repoPath, status);
      
      return status;
    } catch (error) {
      console.error(`Failed to get repository status for ${repoPath}:`, error);
      return this.createEmptyStatus();
    }
  }

  private parseGitStatus(output: string): GitStatus {
    const lines = output.split('\n').filter(line => line.trim());
    const status: GitStatus = {
      branch: 'main',
      ahead: 0,
      behind: 0,
      staged: [],
      unstaged: [],
      untracked: [],
      conflicted: [],
      timestamp: Date.now()
    };

    for (const line of lines) {
      if (line.startsWith('##')) {
        // Branch information
        const branchMatch = line.match(/## ([^.\s]+)/);
        if (branchMatch) {
          status.branch = branchMatch[1];
        }
        
        const aheadMatch = line.match(/ahead (\d+)/);
        if (aheadMatch) {
          status.ahead = parseInt(aheadMatch[1], 10);
        }
        
        const behindMatch = line.match(/behind (\d+)/);
        if (behindMatch) {
          status.behind = parseInt(behindMatch[1], 10);
        }
      } else {
        // File status
        const statusCode = line.substring(0, 2);
        const filePath = line.substring(3);
        
        const fileStatus: GitFileStatus = {
          path: filePath,
          status: this.parseFileStatus(statusCode),
          staged: statusCode[0] !== ' ' && statusCode[0] !== '?',
          unstaged: statusCode[1] !== ' '
        };

        if (statusCode.includes('U') || statusCode.includes('A') && statusCode.includes('A')) {
          status.conflicted.push(fileStatus);
        } else if (fileStatus.staged) {
          status.staged.push(fileStatus);
        } else if (statusCode.startsWith('??')) {
          status.untracked.push(fileStatus);
        } else if (fileStatus.unstaged) {
          status.unstaged.push(fileStatus);
        }
      }
    }

    return status;
  }

  private parseFileStatus(statusCode: string): GitFileChangeType {
    const code = statusCode.replace(' ', '');
    switch (code) {
      case 'M': return 'modified';
      case 'A': return 'added';
      case 'D': return 'deleted';
      case 'R': return 'renamed';
      case 'C': return 'copied';
      case 'U': return 'unmerged';
      case '??': return 'untracked';
      default: return 'modified';
    }
  }

  private createEmptyStatus(): GitStatus {
    return {
      branch: 'main',
      ahead: 0,
      behind: 0,
      staged: [],
      unstaged: [],
      untracked: [],
      conflicted: [],
      timestamp: Date.now()
    };
  }

  // Branch operations
  async getBranches(repoPath: string): Promise<GitBranch[]> {
    try {
      const result = await this.executeGitCommand(repoPath, ['branch', '-a', '-v']);
      return this.parseBranches(result.stdout);
    } catch (error) {
      console.error(`Failed to get branches for ${repoPath}:`, error);
      return [];
    }
  }

  private parseBranches(output: string): GitBranch[] {
    const lines = output.split('\n').filter(line => line.trim());
    const branches: GitBranch[] = [];

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed) continue;

      const isActive = trimmed.startsWith('*');
      const cleanLine = trimmed.replace(/^\*\s*/, '');
      const parts = cleanLine.split(/\s+/);
      
      if (parts.length >= 2) {
        const name = parts[0];
        const commit = parts[1];
        const isRemote = name.startsWith('remotes/');
        
        branches.push({
          name: isRemote ? name.replace('remotes/', '') : name,
          commit,
          isActive,
          isRemote,
          upstream: isRemote ? undefined : this.getUpstreamBranch(name)
        });
      }
    }

    return branches;
  }

  private getUpstreamBranch(branchName: string): string | undefined {
    // This would typically be determined from git config
    // For now, return a simple heuristic
    return `origin/${branchName}`;
  }

  async createBranch(repoPath: string, branchName: string, startPoint?: string): Promise<boolean> {
    try {
      const args = ['checkout', '-b', branchName];
      if (startPoint) {
        args.push(startPoint);
      }
      
      await this.executeGitCommand(repoPath, args);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Created branch: ${branchName}`);
      this.eventEmitter.fire({ type: 'branchCreated', repoPath, branchName });
      
      return true;
    } catch (error) {
      console.error(`Failed to create branch ${branchName}:`, error);
      return false;
    }
  }

  async switchBranch(repoPath: string, branchName: string): Promise<boolean> {
    try {
      await this.executeGitCommand(repoPath, ['checkout', branchName]);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Switched to branch: ${branchName}`);
      this.eventEmitter.fire({ type: 'branchSwitched', repoPath, branchName });
      
      return true;
    } catch (error) {
      console.error(`Failed to switch to branch ${branchName}:`, error);
      return false;
    }
  }

  async deleteBranch(repoPath: string, branchName: string, force = false): Promise<boolean> {
    try {
      const args = ['branch', force ? '-D' : '-d', branchName];
      await this.executeGitCommand(repoPath, args);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Deleted branch: ${branchName}`);
      this.eventEmitter.fire({ type: 'branchDeleted', repoPath, branchName });
      
      return true;
    } catch (error) {
      console.error(`Failed to delete branch ${branchName}:`, error);
      return false;
    }
  }

  // Commit operations
  async stageFiles(repoPath: string, filePaths: string[]): Promise<boolean> {
    try {
      const args = ['add', ...filePaths];
      await this.executeGitCommand(repoPath, args);
      
      // Refresh status
      this.statusCache.delete(repoPath);
      
      console.log(`Staged files: ${filePaths.join(', ')}`);
      this.eventEmitter.fire({ type: 'filesStaged', repoPath, filePaths });
      
      return true;
    } catch (error) {
      console.error(`Failed to stage files:`, error);
      return false;
    }
  }

  async unstageFiles(repoPath: string, filePaths: string[]): Promise<boolean> {
    try {
      const args = ['reset', 'HEAD', ...filePaths];
      await this.executeGitCommand(repoPath, args);
      
      // Refresh status
      this.statusCache.delete(repoPath);
      
      console.log(`Unstaged files: ${filePaths.join(', ')}`);
      this.eventEmitter.fire({ type: 'filesUnstaged', repoPath, filePaths });
      
      return true;
    } catch (error) {
      console.error(`Failed to unstage files:`, error);
      return false;
    }
  }

  async commit(repoPath: string, message: string, options: GitCommitOptions = {}): Promise<boolean> {
    try {
      const args = ['commit', '-m', message];
      
      if (options.amend) {
        args.push('--amend');
      }
      
      if (options.signOff) {
        args.push('--signoff');
      }
      
      if (options.all) {
        args.push('-a');
      }
      
      const result = await this.executeGitCommand(repoPath, args);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Committed: ${message}`);
      this.eventEmitter.fire({ type: 'committed', repoPath, message, hash: this.extractCommitHash(result.stdout) });
      
      return true;
    } catch (error) {
      console.error(`Failed to commit:`, error);
      return false;
    }
  }

  private extractCommitHash(output: string): string {
    const match = output.match(/\[\w+\s+([a-f0-9]+)\]/);
    return match ? match[1] : '';
  }

  // Remote operations
  async getRemotes(repoPath: string): Promise<GitRemote[]> {
    try {
      const result = await this.executeGitCommand(repoPath, ['remote', '-v']);
      return this.parseRemotes(result.stdout);
    } catch (error) {
      console.error(`Failed to get remotes for ${repoPath}:`, error);
      return [];
    }
  }

  private parseRemotes(output: string): GitRemote[] {
    const lines = output.split('\n').filter(line => line.trim());
    const remoteMap = new Map<string, GitRemote>();

    for (const line of lines) {
      const parts = line.split(/\s+/);
      if (parts.length >= 3) {
        const name = parts[0];
        const url = parts[1];
        const type = parts[2].replace(/[()]/g, '');
        
        let remote = remoteMap.get(name);
        if (!remote) {
          remote = { name, fetchUrl: '', pushUrl: '' };
          remoteMap.set(name, remote);
        }
        
        if (type === 'fetch') {
          remote.fetchUrl = url;
        } else if (type === 'push') {
          remote.pushUrl = url;
        }
      }
    }

    return Array.from(remoteMap.values());
  }

  async fetch(repoPath: string, remote = 'origin'): Promise<boolean> {
    try {
      await this.executeGitCommand(repoPath, ['fetch', remote]);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Fetched from ${remote}`);
      this.eventEmitter.fire({ type: 'fetched', repoPath, remote });
      
      return true;
    } catch (error) {
      console.error(`Failed to fetch from ${remote}:`, error);
      return false;
    }
  }

  async pull(repoPath: string, remote = 'origin', branch?: string): Promise<boolean> {
    try {
      const args = ['pull', remote];
      if (branch) {
        args.push(branch);
      }
      
      await this.executeGitCommand(repoPath, args);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Pulled from ${remote}`);
      this.eventEmitter.fire({ type: 'pulled', repoPath, remote, branch });
      
      return true;
    } catch (error) {
      console.error(`Failed to pull from ${remote}:`, error);
      return false;
    }
  }

  async push(repoPath: string, remote = 'origin', branch?: string, options: GitPushOptions = {}): Promise<boolean> {
    try {
      const args = ['push'];
      
      if (options.force) {
        args.push('--force');
      }
      
      if (options.setUpstream) {
        args.push('--set-upstream');
      }
      
      args.push(remote);
      
      if (branch) {
        args.push(branch);
      }
      
      await this.executeGitCommand(repoPath, args);
      
      // Refresh repository data
      await this.refreshRepository(repoPath);
      
      console.log(`Pushed to ${remote}`);
      this.eventEmitter.fire({ type: 'pushed', repoPath, remote, branch });
      
      return true;
    } catch (error) {
      console.error(`Failed to push to ${remote}:`, error);
      return false;
    }
  }

  // Diff operations
  async getDiff(repoPath: string, filePath?: string, staged = false): Promise<string> {
    try {
      const args = ['diff'];
      
      if (staged) {
        args.push('--staged');
      }
      
      if (filePath) {
        args.push('--', filePath);
      }
      
      const result = await this.executeGitCommand(repoPath, args);
      return result.stdout;
    } catch (error) {
      console.error(`Failed to get diff:`, error);
      return '';
    }
  }

  async getFileHistory(repoPath: string, filePath: string, maxCount = 50): Promise<GitCommit[]> {
    try {
      const args = [
        'log',
        '--follow',
        '--pretty=format:%H|%an|%ae|%ad|%s',
        '--date=iso',
        `-n${maxCount}`,
        '--',
        filePath
      ];
      
      const result = await this.executeGitCommand(repoPath, args);
      return this.parseCommitHistory(result.stdout);
    } catch (error) {
      console.error(`Failed to get file history for ${filePath}:`, error);
      return [];
    }
  }

  private parseCommitHistory(output: string): GitCommit[] {
    const lines = output.split('\n').filter(line => line.trim());
    const commits: GitCommit[] = [];

    for (const line of lines) {
      const parts = line.split('|');
      if (parts.length >= 5) {
        commits.push({
          hash: parts[0],
          author: parts[1],
          email: parts[2],
          date: new Date(parts[3]),
          message: parts[4],
          shortHash: parts[0].substring(0, 7)
        });
      }
    }

    return commits;
  }

  // Utility methods
  private async executeGitCommand(repoPath: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
    return new Promise((resolve, reject) => {
      const { spawn } = require('child_process');
      const git = spawn('git', args, {
        cwd: repoPath,
        stdio: ['pipe', 'pipe', 'pipe']
      });

      let stdout = '';
      let stderr = '';

      git.stdout.on('data', (data: Buffer) => {
        stdout += data.toString();
      });

      git.stderr.on('data', (data: Buffer) => {
        stderr += data.toString();
      });

      git.on('close', (code: number) => {
        if (code === 0) {
          resolve({ stdout, stderr });
        } else {
          reject(new Error(`Git command failed with code ${code}: ${stderr}`));
        }
      });

      git.on('error', (error: Error) => {
        reject(error);
      });
    });
  }

  private async getHead(repoPath: string): Promise<string> {
    try {
      const result = await this.executeGitCommand(repoPath, ['rev-parse', 'HEAD']);
      return result.stdout.trim();
    } catch (error) {
      return '';
    }
  }

  private async getConfig(repoPath: string): Promise<GitConfig> {
    try {
      const result = await this.executeGitCommand(repoPath, ['config', '--list']);
      return this.parseConfig(result.stdout);
    } catch (error) {
      return {};
    }
  }

  private parseConfig(output: string): GitConfig {
    const config: GitConfig = {};
    const lines = output.split('\n').filter(line => line.trim());

    for (const line of lines) {
      const [key, value] = line.split('=', 2);
      if (key && value) {
        config[key] = value;
      }
    }

    return config;
  }

  private async refreshRepository(repoPath: string): Promise<void> {
    const repository = this.repositories.get(repoPath);
    if (repository) {
      repository.status = await this.getRepositoryStatus(repoPath);
      repository.branches = await this.getBranches(repoPath);
      repository.head = await this.getHead(repoPath);
      repository.lastUpdated = Date.now();
      
      this.eventEmitter.fire({ type: 'repositoryUpdated', repository });
    }
  }

  private setupFileWatchers(): void {
    // Watch for changes in Git repositories
    for (const repoPath of this.repositories.keys()) {
      const pattern = new TraeAPI.RelativePattern(repoPath, '**/*');
      const watcher = TraeAPI.workspace.createFileSystemWatcher(pattern);
      
      watcher.onDidChange(() => this.onFileChanged(repoPath));
      watcher.onDidCreate(() => this.onFileChanged(repoPath));
      watcher.onDidDelete(() => this.onFileChanged(repoPath));
      
      this.watchers.set(repoPath, watcher);
    }
  }

  private setupEventListeners(): void {
    // Listen for workspace changes
    TraeAPI.workspace.onDidChangeWorkspaceFolders(event => {
      event.added.forEach(folder => {
        this.discoverRepository(folder.uri.fsPath);
      });
      
      event.removed.forEach(folder => {
        this.repositories.delete(folder.uri.fsPath);
        const watcher = this.watchers.get(folder.uri.fsPath);
        if (watcher) {
          watcher.dispose();
          this.watchers.delete(folder.uri.fsPath);
        }
      });
    });
  }

  private onFileChanged(repoPath: string): void {
    // Debounce status updates
    clearTimeout(this.statusUpdateTimeouts.get(repoPath));
    const timeout = setTimeout(() => {
      this.statusCache.delete(repoPath);
      this.eventEmitter.fire({ type: 'statusChanged', repoPath });
    }, 500);
    this.statusUpdateTimeouts.set(repoPath, timeout);
  }

  private statusUpdateTimeouts = new Map<string, NodeJS.Timeout>();

  // Event handling
  onDidChangeRepository(listener: (event: GitEvent) => void): TraeAPI.Disposable {
    return this.eventEmitter.event(listener);
  }

  dispose(): void {
    this.repositories.clear();
    this.statusCache.clear();
    
    for (const watcher of this.watchers.values()) {
      watcher.dispose();
    }
    this.watchers.clear();
    
    for (const timeout of this.statusUpdateTimeouts.values()) {
      clearTimeout(timeout);
    }
    this.statusUpdateTimeouts.clear();
    
    this.eventEmitter.dispose();
  }
}

// Interfaces
interface GitRepository {
  rootPath: string;
  gitPath: string;
  name: string;
  status: GitStatus;
  branches: GitBranch[];
  remotes: GitRemote[];
  head: string;
  config: GitConfig;
  lastUpdated: number;
}

interface GitStatus {
  branch: string;
  ahead: number;
  behind: number;
  staged: GitFileStatus[];
  unstaged: GitFileStatus[];
  untracked: GitFileStatus[];
  conflicted: GitFileStatus[];
  timestamp: number;
}

interface GitFileStatus {
  path: string;
  status: GitFileChangeType;
  staged: boolean;
  unstaged: boolean;
}

type GitFileChangeType = 'modified' | 'added' | 'deleted' | 'renamed' | 'copied' | 'unmerged' | 'untracked';

interface GitBranch {
  name: string;
  commit: string;
  isActive: boolean;
  isRemote: boolean;
  upstream?: string;
}

interface GitRemote {
  name: string;
  fetchUrl: string;
  pushUrl: string;
}

interface GitCommit {
  hash: string;
  shortHash: string;
  author: string;
  email: string;
  date: Date;
  message: string;
}

interface GitConfig {
  [key: string]: string;
}

interface GitCommitOptions {
  amend?: boolean;
  signOff?: boolean;
  all?: boolean;
}

interface GitPushOptions {
  force?: boolean;
  setUpstream?: boolean;
}

type GitEvent = {
  type: 'repositoryDiscovered';
  repository: GitRepository;
} | {
  type: 'repositoryUpdated';
  repository: GitRepository;
} | {
  type: 'statusChanged';
  repoPath: string;
} | {
  type: 'branchCreated' | 'branchSwitched' | 'branchDeleted';
  repoPath: string;
  branchName: string;
} | {
  type: 'filesStaged' | 'filesUnstaged';
  repoPath: string;
  filePaths: string[];
} | {
  type: 'committed';
  repoPath: string;
  message: string;
  hash: string;
} | {
  type: 'fetched' | 'pushed';
  repoPath: string;
  remote: string;
  branch?: string;
} | {
  type: 'pulled';
  repoPath: string;
  remote: string;
  branch?: string;
};

type GitEvents = GitEvent;

// Initialize Git manager
const gitManager = new GitManager();

Git UI Integration

typescript
// Git UI components
class GitUIProvider {
  private statusBarItem: TraeAPI.StatusBarItem;
  private treeDataProvider: GitTreeDataProvider;

  constructor(private gitManager: GitManager) {
    this.statusBarItem = TraeAPI.window.createStatusBarItem(TraeAPI.StatusBarAlignment.Left, 100);
    this.treeDataProvider = new GitTreeDataProvider(gitManager);
    this.setupUI();
    this.setupEventListeners();
  }

  private setupUI(): void {
    // Register tree view
    TraeAPI.window.createTreeView('git.repositories', {
      treeDataProvider: this.treeDataProvider,
      showCollapseAll: true
    });

    // Setup status bar
    this.updateStatusBar();
    this.statusBarItem.show();
  }

  private setupEventListeners(): void {
    this.gitManager.onDidChangeRepository(() => {
      this.updateStatusBar();
      this.treeDataProvider.refresh();
    });
  }

  private async updateStatusBar(): Promise<void> {
    const activeEditor = TraeAPI.window.activeTextEditor;
    if (!activeEditor) {
      this.statusBarItem.hide();
      return;
    }

    const repository = await this.gitManager.getRepositoryForFile(activeEditor.document.fileName);
    if (!repository) {
      this.statusBarItem.hide();
      return;
    }

    const status = repository.status;
    const branchIcon = '$(git-branch)';
    const syncIcon = status.ahead > 0 || status.behind > 0 ? '$(sync)' : '';
    const changesIcon = status.staged.length + status.unstaged.length > 0 ? '$(circle-filled)' : '';
    
    this.statusBarItem.text = `${branchIcon} ${status.branch} ${syncIcon} ${changesIcon}`.trim();
    this.statusBarItem.tooltip = this.createStatusTooltip(status);
    this.statusBarItem.command = 'git.showQuickPick';
    this.statusBarItem.show();
  }

  private createStatusTooltip(status: GitStatus): string {
    const parts = [`Branch: ${status.branch}`];
    
    if (status.ahead > 0) {
      parts.push(`Ahead: ${status.ahead}`);
    }
    
    if (status.behind > 0) {
      parts.push(`Behind: ${status.behind}`);
    }
    
    if (status.staged.length > 0) {
      parts.push(`Staged: ${status.staged.length}`);
    }
    
    if (status.unstaged.length > 0) {
      parts.push(`Unstaged: ${status.unstaged.length}`);
    }
    
    if (status.untracked.length > 0) {
      parts.push(`Untracked: ${status.untracked.length}`);
    }
    
    return parts.join('\n');
  }
}

class GitTreeDataProvider implements TraeAPI.TreeDataProvider<GitTreeItem> {
  private _onDidChangeTreeData = new TraeAPI.EventEmitter<GitTreeItem | undefined | null | void>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  constructor(private gitManager: GitManager) {}

  refresh(): void {
    this._onDidChangeTreeData.fire();
  }

  getTreeItem(element: GitTreeItem): TraeAPI.TreeItem {
    return element;
  }

  async getChildren(element?: GitTreeItem): Promise<GitTreeItem[]> {
    if (!element) {
      // Root level - show repositories
      const repositories = this.gitManager.getAllRepositories();
      return repositories.map(repo => new GitRepositoryItem(repo));
    }

    if (element instanceof GitRepositoryItem) {
      // Repository level - show categories
      return [
        new GitCategoryItem('Changes', element.repository, 'changes'),
        new GitCategoryItem('Branches', element.repository, 'branches'),
        new GitCategoryItem('Remotes', element.repository, 'remotes')
      ];
    }

    if (element instanceof GitCategoryItem) {
      switch (element.category) {
        case 'changes':
          return this.getChangeItems(element.repository);
        case 'branches':
          return this.getBranchItems(element.repository);
        case 'remotes':
          return this.getRemoteItems(element.repository);
      }
    }

    return [];
  }

  private getChangeItems(repository: GitRepository): GitTreeItem[] {
    const items: GitTreeItem[] = [];
    const status = repository.status;

    if (status.staged.length > 0) {
      items.push(new GitChangeCategoryItem('Staged Changes', status.staged));
    }

    if (status.unstaged.length > 0) {
      items.push(new GitChangeCategoryItem('Changes', status.unstaged));
    }

    if (status.untracked.length > 0) {
      items.push(new GitChangeCategoryItem('Untracked Files', status.untracked));
    }

    return items;
  }

  private getBranchItems(repository: GitRepository): GitTreeItem[] {
    return repository.branches
      .filter(branch => !branch.isRemote)
      .map(branch => new GitBranchItem(branch, repository));
  }

  private getRemoteItems(repository: GitRepository): GitTreeItem[] {
    return repository.remotes.map(remote => new GitRemoteItem(remote, repository));
  }
}

// Tree item classes
abstract class GitTreeItem extends TraeAPI.TreeItem {
  constructor(
    public readonly label: string,
    public readonly collapsibleState: TraeAPI.TreeItemCollapsibleState
  ) {
    super(label, collapsibleState);
  }
}

class GitRepositoryItem extends GitTreeItem {
  constructor(public readonly repository: GitRepository) {
    super(repository.name, TraeAPI.TreeItemCollapsibleState.Expanded);
    this.tooltip = repository.rootPath;
    this.contextValue = 'repository';
    this.iconPath = new TraeAPI.ThemeIcon('repo');
  }
}

class GitCategoryItem extends GitTreeItem {
  constructor(
    label: string,
    public readonly repository: GitRepository,
    public readonly category: 'changes' | 'branches' | 'remotes'
  ) {
    super(label, TraeAPI.TreeItemCollapsibleState.Collapsed);
    this.contextValue = `category-${category}`;
  }
}

class GitChangeCategoryItem extends GitTreeItem {
  constructor(label: string, public readonly files: GitFileStatus[]) {
    super(`${label} (${files.length})`, TraeAPI.TreeItemCollapsibleState.Expanded);
    this.contextValue = 'changeCategory';
  }

  async getChildren(): Promise<GitFileItem[]> {
    return this.files.map(file => new GitFileItem(file));
  }
}

class GitFileItem extends GitTreeItem {
  constructor(public readonly fileStatus: GitFileStatus) {
    super(TraeAPI.path.basename(fileStatus.path), TraeAPI.TreeItemCollapsibleState.None);
    this.tooltip = fileStatus.path;
    this.contextValue = 'file';
    this.resourceUri = TraeAPI.Uri.file(fileStatus.path);
    this.command = {
      command: 'vscode.open',
      title: 'Open File',
      arguments: [this.resourceUri]
    };
    
    // Set icon based on file status
    this.iconPath = this.getStatusIcon(fileStatus.status);
  }

  private getStatusIcon(status: GitFileChangeType): TraeAPI.ThemeIcon {
    switch (status) {
      case 'modified': return new TraeAPI.ThemeIcon('diff-modified');
      case 'added': return new TraeAPI.ThemeIcon('diff-added');
      case 'deleted': return new TraeAPI.ThemeIcon('diff-removed');
      case 'renamed': return new TraeAPI.ThemeIcon('diff-renamed');
      case 'untracked': return new TraeAPI.ThemeIcon('question');
      case 'unmerged': return new TraeAPI.ThemeIcon('warning');
      default: return new TraeAPI.ThemeIcon('file');
    }
  }
}

class GitBranchItem extends GitTreeItem {
  constructor(public readonly branch: GitBranch, public readonly repository: GitRepository) {
    super(branch.name, TraeAPI.TreeItemCollapsibleState.None);
    this.tooltip = `${branch.name} (${branch.commit})`;
    this.contextValue = branch.isActive ? 'activeBranch' : 'branch';
    this.iconPath = new TraeAPI.ThemeIcon(branch.isActive ? 'check' : 'git-branch');
    
    if (branch.isActive) {
      this.label = `● ${branch.name}`;
    }
  }
}

class GitRemoteItem extends GitTreeItem {
  constructor(public readonly remote: GitRemote, public readonly repository: GitRepository) {
    super(remote.name, TraeAPI.TreeItemCollapsibleState.None);
    this.tooltip = `${remote.name}\nFetch: ${remote.fetchUrl}\nPush: ${remote.pushUrl}`;
    this.contextValue = 'remote';
    this.iconPath = new TraeAPI.ThemeIcon('cloud');
  }
}

// Initialize Git UI
const gitUIProvider = new GitUIProvider(gitManager);

API Reference

Core Interfaces

typescript
interface GitAPI {
  // Repository management
  getRepository(path: string): Promise<GitRepository | null>;
  getRepositoryForFile(filePath: string): Promise<GitRepository | null>;
  getAllRepositories(): GitRepository[];
  
  // Status operations
  getRepositoryStatus(repoPath: string): Promise<GitStatus>;
  
  // Branch operations
  getBranches(repoPath: string): Promise<GitBranch[]>;
  createBranch(repoPath: string, branchName: string, startPoint?: string): Promise<boolean>;
  switchBranch(repoPath: string, branchName: string): Promise<boolean>;
  deleteBranch(repoPath: string, branchName: string, force?: boolean): Promise<boolean>;
  
  // Commit operations
  stageFiles(repoPath: string, filePaths: string[]): Promise<boolean>;
  unstageFiles(repoPath: string, filePaths: string[]): Promise<boolean>;
  commit(repoPath: string, message: string, options?: GitCommitOptions): Promise<boolean>;
  
  // Remote operations
  getRemotes(repoPath: string): Promise<GitRemote[]>;
  fetch(repoPath: string, remote?: string): Promise<boolean>;
  pull(repoPath: string, remote?: string, branch?: string): Promise<boolean>;
  push(repoPath: string, remote?: string, branch?: string, options?: GitPushOptions): Promise<boolean>;
  
  // Diff and history
  getDiff(repoPath: string, filePath?: string, staged?: boolean): Promise<string>;
  getFileHistory(repoPath: string, filePath: string, maxCount?: number): Promise<GitCommit[]>;
  
  // Events
  onDidChangeRepository(listener: (event: GitEvent) => void): TraeAPI.Disposable;
}

Best Practices

  1. Performance: Cache repository status and use file watchers for updates
  2. Error Handling: Gracefully handle Git command failures
  3. User Experience: Provide clear feedback for Git operations
  4. Security: Validate Git commands and paths
  5. Integration: Integrate with editor decorations and diff views
  6. Customization: Allow users to configure Git behavior
  7. Accessibility: Ensure Git UI is accessible
  8. Testing: Test Git operations with various repository states

Your Ultimate AI-Powered IDE Learning Guide