源代码管理 API
Trae IDE 的源代码管理 (SCM) API 允许扩展集成版本控制系统,如 Git、SVN 等。
概述
SCM 功能包括:
- 源代码管理提供者注册
- 文件状态跟踪
- 变更管理
- 提交和推送操作
- 分支管理
- 冲突解决
主要接口
SourceControl
typescript
interface SourceControl {
// 基本信息
readonly id: string
readonly label: string
readonly rootUri?: Uri
// 输入框
readonly inputBox: SourceControlInputBox
// 资源组
readonly count?: number
// 快速差异提供者
quickDiffProvider?: QuickDiffProvider
// 接受输入命令
acceptInputCommand?: Command
// 状态栏命令
statusBarCommands?: Command[]
// 操作方法
createResourceGroup(id: string, label: string): SourceControlResourceGroup
dispose(): void
}SourceControlResourceGroup
typescript
interface SourceControlResourceGroup {
readonly id: string
label: string
hideWhenEmpty?: boolean
// 资源状态
resourceStates: SourceControlResourceState[]
// 销毁
dispose(): void
}SourceControlResourceState
typescript
interface SourceControlResourceState {
readonly resourceUri: Uri
readonly command?: Command
readonly decorations?: SourceControlResourceDecorations
}创建 SCM 提供者
基本创建
typescript
import { scm, SourceControl, SourceControlResourceGroup } from 'trae-api'
class GitSCMProvider {
private sourceControl: SourceControl
private changesGroup: SourceControlResourceGroup
private stagedGroup: SourceControlResourceGroup
constructor(workspaceRoot: Uri) {
// 创建源代码管理实例
this.sourceControl = scm.createSourceControl(
'git', // 唯一标识符
'Git', // 显示名称
workspaceRoot // 工作区根目录
)
// 创建资源组
this.changesGroup = this.sourceControl.createResourceGroup(
'workingTree',
'更改'
)
this.stagedGroup = this.sourceControl.createResourceGroup(
'index',
'暂存的更改'
)
this.setupInputBox()
this.setupCommands()
}
private setupInputBox() {
// 设置提交消息输入框
this.sourceControl.inputBox.placeholder = '提交消息'
this.sourceControl.acceptInputCommand = {
command: 'git.commit',
title: '提交',
arguments: [this.sourceControl]
}
}
private setupCommands() {
// 设置状态栏命令
this.sourceControl.statusBarCommands = [
{
command: 'git.sync',
title: '$(sync) 同步',
tooltip: '拉取和推送更改'
},
{
command: 'git.publish',
title: '$(cloud-upload) 发布',
tooltip: '发布分支'
}
]
}
}高级配置
typescript
class AdvancedSCMProvider {
private sourceControl: SourceControl
constructor(workspaceRoot: Uri) {
this.sourceControl = scm.createSourceControl('advanced-git', 'Advanced Git', workspaceRoot)
// 设置快速差异提供者
this.sourceControl.quickDiffProvider = {
provideOriginalResource: (uri: Uri) => {
// 返回原始文件内容的 URI
return Uri.parse(`git:${uri.path}?HEAD`)
}
}
// 设置计数
this.sourceControl.count = 5 // 显示未提交更改数量
this.setupResourceGroups()
}
private setupResourceGroups() {
// 工作区更改
const workingTreeGroup = this.sourceControl.createResourceGroup(
'workingTree',
'工作区更改'
)
workingTreeGroup.hideWhenEmpty = true
// 暂存区更改
const indexGroup = this.sourceControl.createResourceGroup(
'index',
'暂存区更改'
)
indexGroup.hideWhenEmpty = true
// 合并冲突
const mergeGroup = this.sourceControl.createResourceGroup(
'merge',
'合并冲突'
)
mergeGroup.hideWhenEmpty = true
}
}资源状态管理
文件状态跟踪
typescript
import { SourceControlResourceDecorations, ThemeColor } from 'trae-api'
class FileStatusTracker {
private workingTreeGroup: SourceControlResourceGroup
private indexGroup: SourceControlResourceGroup
constructor(sourceControl: SourceControl) {
this.workingTreeGroup = sourceControl.createResourceGroup('workingTree', '更改')
this.indexGroup = sourceControl.createResourceGroup('index', '暂存的更改')
}
updateFileStatus(uri: Uri, status: GitFileStatus) {
const decorations = this.getDecorations(status)
const command = this.getCommand(uri, status)
const resourceState: SourceControlResourceState = {
resourceUri: uri,
command: command,
decorations: decorations
}
// 根据状态添加到相应的组
if (status.staged) {
this.addToGroup(this.indexGroup, resourceState)
} else {
this.addToGroup(this.workingTreeGroup, resourceState)
}
}
private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
const decorations: SourceControlResourceDecorations = {}
switch (status.type) {
case 'modified':
decorations.iconPath = new ThemeIcon('diff-modified')
decorations.tooltip = '已修改'
decorations.faded = false
break
case 'added':
decorations.iconPath = new ThemeIcon('diff-added')
decorations.tooltip = '新增文件'
decorations.faded = false
break
case 'deleted':
decorations.iconPath = new ThemeIcon('diff-removed')
decorations.tooltip = '已删除'
decorations.faded = true
break
case 'renamed':
decorations.iconPath = new ThemeIcon('diff-renamed')
decorations.tooltip = `重命名: ${status.originalPath} → ${status.path}`
break
case 'untracked':
decorations.iconPath = new ThemeIcon('diff-added')
decorations.tooltip = '未跟踪的文件'
decorations.faded = false
break
case 'conflicted':
decorations.iconPath = new ThemeIcon('warning')
decorations.tooltip = '合并冲突'
decorations.faded = false
break
}
return decorations
}
private getCommand(uri: Uri, status: GitFileStatus): Command {
return {
command: 'vscode.diff',
title: '打开更改',
arguments: [
Uri.parse(`git:${uri.path}?HEAD`), // 原始版本
uri, // 当前版本
`${path.basename(uri.path)} (工作区)`
]
}
}
private addToGroup(group: SourceControlResourceGroup, resource: SourceControlResourceState) {
// 检查是否已存在
const existing = group.resourceStates.find(r => r.resourceUri.toString() === resource.resourceUri.toString())
if (existing) {
// 更新现有资源
const index = group.resourceStates.indexOf(existing)
group.resourceStates[index] = resource
} else {
// 添加新资源
group.resourceStates = [...group.resourceStates, resource]
}
}
}
interface GitFileStatus {
type: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'conflicted'
path: string
originalPath?: string
staged: boolean
}批量状态更新
typescript
class BatchStatusUpdater {
private sourceControl: SourceControl
private updateTimer: NodeJS.Timeout | undefined
private pendingUpdates = new Map<string, GitFileStatus>()
constructor(sourceControl: SourceControl) {
this.sourceControl = sourceControl
}
scheduleUpdate(uri: Uri, status: GitFileStatus) {
// 收集待更新的状态
this.pendingUpdates.set(uri.toString(), status)
// 防抖处理
if (this.updateTimer) {
clearTimeout(this.updateTimer)
}
this.updateTimer = setTimeout(() => {
this.performBatchUpdate()
}, 100)
}
private performBatchUpdate() {
const workingTreeResources: SourceControlResourceState[] = []
const indexResources: SourceControlResourceState[] = []
this.pendingUpdates.forEach((status, uriString) => {
const uri = Uri.parse(uriString)
const resource = this.createResourceState(uri, status)
if (status.staged) {
indexResources.push(resource)
} else {
workingTreeResources.push(resource)
}
})
// 批量更新资源组
const workingTreeGroup = this.sourceControl.createResourceGroup('workingTree', '更改')
const indexGroup = this.sourceControl.createResourceGroup('index', '暂存的更改')
workingTreeGroup.resourceStates = workingTreeResources
indexGroup.resourceStates = indexResources
// 更新计数
this.sourceControl.count = workingTreeResources.length + indexResources.length
// 清理
this.pendingUpdates.clear()
this.updateTimer = undefined
}
private createResourceState(uri: Uri, status: GitFileStatus): SourceControlResourceState {
return {
resourceUri: uri,
command: {
command: 'vscode.diff',
title: '查看更改',
arguments: [
Uri.parse(`git:${uri.path}?HEAD`),
uri,
`${path.basename(uri.path)} (工作区)`
]
},
decorations: this.getDecorations(status)
}
}
private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
// 装饰逻辑(同上面的示例)
return {}
}
}实用示例
Git 集成
typescript
class GitIntegration {
private sourceControl: SourceControl
private workingTreeGroup: SourceControlResourceGroup
private indexGroup: SourceControlResourceGroup
private disposables: Disposable[] = []
constructor(workspaceRoot: Uri) {
this.sourceControl = scm.createSourceControl('git', 'Git', workspaceRoot)
this.setupGroups()
this.setupCommands()
this.setupEventListeners()
this.refreshStatus()
}
private setupGroups() {
this.workingTreeGroup = this.sourceControl.createResourceGroup(
'workingTree',
'更改'
)
this.indexGroup = this.sourceControl.createResourceGroup(
'index',
'暂存的更改'
)
// 设置输入框
this.sourceControl.inputBox.placeholder = '消息 (按 Ctrl+Enter 提交)'
this.sourceControl.acceptInputCommand = {
command: 'git.commit',
title: '提交',
arguments: [this.sourceControl]
}
}
private setupCommands() {
// 注册命令
this.disposables.push(
commands.registerCommand('git.commit', async (sourceControl: SourceControl) => {
await this.commit(sourceControl.inputBox.value)
})
)
this.disposables.push(
commands.registerCommand('git.stage', async (resource: SourceControlResourceState) => {
await this.stageFile(resource.resourceUri)
})
)
this.disposables.push(
commands.registerCommand('git.unstage', async (resource: SourceControlResourceState) => {
await this.unstageFile(resource.resourceUri)
})
)
// 设置状态栏命令
this.sourceControl.statusBarCommands = [
{
command: 'git.sync',
title: '$(sync) 同步',
tooltip: '拉取和推送更改'
}
]
}
private setupEventListeners() {
// 监听文件系统变化
this.disposables.push(
workspace.onDidSaveTextDocument(() => {
this.refreshStatus()
})
)
this.disposables.push(
workspace.onDidCreateFiles(() => {
this.refreshStatus()
})
)
this.disposables.push(
workspace.onDidDeleteFiles(() => {
this.refreshStatus()
})
)
}
private async refreshStatus() {
try {
const status = await this.getGitStatus()
this.updateResourceGroups(status)
} catch (error) {
console.error('刷新 Git 状态失败:', error)
}
}
private async getGitStatus(): Promise<GitFileStatus[]> {
// 执行 git status 命令
const result = await this.executeGitCommand(['status', '--porcelain'])
return this.parseGitStatus(result)
}
private parseGitStatus(output: string): GitFileStatus[] {
const statuses: GitFileStatus[] = []
const lines = output.split('\n').filter(line => line.trim())
for (const line of lines) {
const indexStatus = line[0]
const workingTreeStatus = line[1]
const filePath = line.substring(3)
if (indexStatus !== ' ') {
statuses.push({
type: this.getStatusType(indexStatus),
path: filePath,
staged: true
})
}
if (workingTreeStatus !== ' ') {
statuses.push({
type: this.getStatusType(workingTreeStatus),
path: filePath,
staged: false
})
}
}
return statuses
}
private getStatusType(status: string): GitFileStatus['type'] {
switch (status) {
case 'M': return 'modified'
case 'A': return 'added'
case 'D': return 'deleted'
case 'R': return 'renamed'
case '?': return 'untracked'
case 'U': return 'conflicted'
default: return 'modified'
}
}
private updateResourceGroups(statuses: GitFileStatus[]) {
const workingTreeResources: SourceControlResourceState[] = []
const indexResources: SourceControlResourceState[] = []
for (const status of statuses) {
const uri = Uri.file(path.join(this.sourceControl.rootUri!.fsPath, status.path))
const resource = this.createResourceState(uri, status)
if (status.staged) {
indexResources.push(resource)
} else {
workingTreeResources.push(resource)
}
}
this.workingTreeGroup.resourceStates = workingTreeResources
this.indexGroup.resourceStates = indexResources
// 更新计数
this.sourceControl.count = workingTreeResources.length + indexResources.length
}
private createResourceState(uri: Uri, status: GitFileStatus): SourceControlResourceState {
const command = status.staged ? 'git.unstage' : 'git.stage'
return {
resourceUri: uri,
command: {
command: command,
title: status.staged ? '取消暂存' : '暂存更改',
arguments: [{ resourceUri: uri }]
},
decorations: this.getDecorations(status)
}
}
private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
const decorations: SourceControlResourceDecorations = {}
switch (status.type) {
case 'modified':
decorations.iconPath = new ThemeIcon('diff-modified')
decorations.tooltip = '已修改'
break
case 'added':
decorations.iconPath = new ThemeIcon('diff-added')
decorations.tooltip = '新增文件'
break
case 'deleted':
decorations.iconPath = new ThemeIcon('diff-removed')
decorations.tooltip = '已删除'
decorations.faded = true
break
case 'untracked':
decorations.iconPath = new ThemeIcon('diff-added')
decorations.tooltip = '未跟踪的文件'
break
}
return decorations
}
private async commit(message: string) {
if (!message.trim()) {
window.showErrorMessage('请输入提交消息')
return
}
try {
await this.executeGitCommand(['commit', '-m', message])
this.sourceControl.inputBox.value = ''
this.refreshStatus()
window.showInformationMessage('提交成功')
} catch (error) {
window.showErrorMessage(`提交失败: ${error}`)
}
}
private async stageFile(uri: Uri) {
try {
const relativePath = path.relative(this.sourceControl.rootUri!.fsPath, uri.fsPath)
await this.executeGitCommand(['add', relativePath])
this.refreshStatus()
} catch (error) {
window.showErrorMessage(`暂存文件失败: ${error}`)
}
}
private async unstageFile(uri: Uri) {
try {
const relativePath = path.relative(this.sourceControl.rootUri!.fsPath, uri.fsPath)
await this.executeGitCommand(['reset', 'HEAD', relativePath])
this.refreshStatus()
} catch (error) {
window.showErrorMessage(`取消暂存失败: ${error}`)
}
}
private async executeGitCommand(args: string[]): Promise<string> {
// 执行 Git 命令的实现
return new Promise((resolve, reject) => {
const { spawn } = require('child_process')
const git = spawn('git', args, {
cwd: this.sourceControl.rootUri!.fsPath
})
let output = ''
let error = ''
git.stdout.on('data', (data: Buffer) => {
output += data.toString()
})
git.stderr.on('data', (data: Buffer) => {
error += data.toString()
})
git.on('close', (code: number) => {
if (code === 0) {
resolve(output)
} else {
reject(new Error(error))
}
})
})
}
dispose() {
this.disposables.forEach(d => d.dispose())
this.sourceControl.dispose()
}
}快速差异提供者
实现快速差异
typescript
class GitQuickDiffProvider implements QuickDiffProvider {
private sourceControl: SourceControl
constructor(sourceControl: SourceControl) {
this.sourceControl = sourceControl
sourceControl.quickDiffProvider = this
}
provideOriginalResource(uri: Uri, token: CancellationToken): ProviderResult<Uri> {
// 返回 HEAD 版本的文件内容
return Uri.parse(`git:${uri.path}?HEAD`)
}
}
// 注册内容提供者来处理 git: scheme
class GitContentProvider implements TextDocumentContentProvider {
async provideTextDocumentContent(uri: Uri): Promise<string> {
const query = new URLSearchParams(uri.query)
const ref = query.get('ref') || 'HEAD'
const filePath = uri.path
try {
// 获取指定版本的文件内容
const content = await this.getFileContent(filePath, ref)
return content
} catch (error) {
return ''
}
}
private async getFileContent(filePath: string, ref: string): Promise<string> {
// 执行 git show 命令获取文件内容
const { spawn } = require('child_process')
return new Promise((resolve, reject) => {
const git = spawn('git', ['show', `${ref}:${filePath}`])
let content = ''
git.stdout.on('data', (data: Buffer) => {
content += data.toString()
})
git.on('close', (code: number) => {
if (code === 0) {
resolve(content)
} else {
reject(new Error('文件不存在或无法访问'))
}
})
})
}
}最佳实践
- 性能优化: 使用防抖机制避免频繁的状态更新
- 用户体验: 提供清晰的文件状态图标和工具提示
- 错误处理: 妥善处理 Git 命令执行失败的情况
- 资源管理: 及时清理事件监听器和定时器
- 扩展性: 设计可扩展的架构支持多种版本控制系统