Git API
Git APIは、開発環境内でのGitバージョン管理統合のための包括的な機能を提供します。
概要
Git APIでは以下のことができます:
- Gitリポジトリ情報へのアクセス
- Git操作の実行(コミット、プッシュ、プルなど)
- リポジトリのステータスと変更の監視
- ブランチとリモートの管理
- マージコンフリクトの処理
- Gitワークフローとの統合
- Git関連UIコンポーネントの提供
- Gitフックと自動化のサポート
基本的な使用方法
リポジトリアクセス
typescript
import { TraeAPI } from '@trae/api';
// Gitリポジトリマネージャー
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> {
// ワークスペース内のGitリポジトリを発見
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 {
// ディレクトリに.gitフォルダが含まれているかチェック
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) {
// Gitリポジトリではないかアクセスエラー
}
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;
}
// リポジトリ操作
async getRepository(path: string): Promise<GitRepository | null> {
// まずキャッシュをチェック
let repository = this.repositories.get(path);
if (repository) {
return repository;
}
// リポジトリの発見を試行
repository = await this.discoverRepository(path);
return repository;
}
async getRepositoryForFile(filePath: string): Promise<GitRepository | null> {
// このファイルを含むリポジトリを検索
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;
}ベストプラクティス
- パフォーマンス: リポジトリステータスをキャッシュし、ファイルウォッチャーを使用して更新する
- エラーハンドリング: Gitコマンドの失敗を適切に処理する
- ユーザーエクスペリエンス: Git操作に対して明確なフィードバックを提供する
- セキュリティ: Gitコマンドとパスを検証する
- 統合: エディターの装飾とdiffビューと統合する
- カスタマイゼーション: ユーザーがGitの動作を設定できるようにする
- アクセシビリティ: Git UIがアクセシブルであることを確保する
- テスト: さまざまなリポジトリ状態でGit操作をテストする
関連API
- Workspace API - ファイルシステム操作用
- UI API - Git UIコンポーネント用
- Commands API - Gitコマンド登録用
- Settings API - Git設定用