Files
openchamber/packages/vscode/webview/api/git.ts
T
Bohdan Triapitsyn c80c2b62a8 feat: add one-click git sync button
Combine fetch, pull with rebase, and push into one sync action
Keep remote dropdown focused on safe fetch actions
Block sync when uncommitted changes would conflict with rebase
2026-05-05 20:45:31 +03:00

413 lines
14 KiB
TypeScript

/**
* VS Code Git API implementation
* Uses bridge messages to communicate with the extension host
*/
import { sendBridgeMessage } from './bridge';
import type {
GitAPI,
GitStatus,
GitDiffResponse,
GetGitDiffOptions,
GitFileDiffResponse,
GetGitFileDiffOptions,
GitBranch,
GitDeleteBranchPayload,
GitDeleteRemoteBranchPayload,
GitRemoveRemotePayload,
GeneratedCommitMessage,
GeneratedPullRequestDescription,
GitWorktreeInfo,
CreateGitWorktreePayload,
GitWorktreeValidationResult,
GitWorktreeCreateResult,
RemoveGitWorktreePayload,
GitCommitResult,
CreateGitCommitOptions,
GitPushResult,
GitPullResult,
GitLogResponse,
GitLogOptions,
GitCommitFilesResponse,
GitIdentitySummary,
GitIdentityProfile,
GitRemote,
GitRebaseResult,
GitMergeResult,
} from '@openchamber/ui/lib/api/types';
export const createVSCodeGitAPI = (): GitAPI => ({
checkIsGitRepository: async (directory: string): Promise<boolean> => {
return sendBridgeMessage<boolean>('api:git/check', { directory });
},
getGitStatus: async (directory: string): Promise<GitStatus> => {
return sendBridgeMessage<GitStatus>('api:git/status', { directory });
},
getGitDiff: async (directory: string, options: GetGitDiffOptions): Promise<GitDiffResponse> => {
return sendBridgeMessage<GitDiffResponse>('api:git/diff', {
directory,
path: options.path,
staged: options.staged,
contextLines: options.contextLines,
});
},
getGitFileDiff: async (directory: string, options: GetGitFileDiffOptions): Promise<GitFileDiffResponse> => {
return sendBridgeMessage<GitFileDiffResponse>('api:git/file-diff', {
directory,
path: options.path,
staged: options.staged,
});
},
revertGitFile: async (directory: string, filePath: string): Promise<void> => {
await sendBridgeMessage('api:git/revert', { directory, path: filePath });
},
isLinkedWorktree: async (directory: string): Promise<boolean> => {
return sendBridgeMessage<boolean>('api:git/worktree-type', { directory });
},
getGitBranches: async (directory: string): Promise<GitBranch> => {
return sendBridgeMessage<GitBranch>('api:git/branches', { directory, method: 'GET' });
},
deleteGitBranch: async (directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/branches', {
directory,
method: 'DELETE',
name: payload.branch,
force: payload.force,
});
},
deleteRemoteBranch: async (directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/remote-branches', {
directory,
branch: payload.branch,
remote: payload.remote,
});
},
removeRemote: async (directory: string, payload: GitRemoveRemotePayload): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/remotes', {
directory,
method: 'DELETE',
remote: payload.remote,
});
},
generateCommitMessage: async (
directory: string,
files: string[],
options?: { zenModel?: string; providerId?: string; modelId?: string }
): Promise<{ message: GeneratedCommitMessage }> => {
// This requires AI integration - stubbed for now
void directory; // Unused for now
void files; // Unused for now
void options; // Unused for now
return {
message: {
subject: '',
highlights: [],
},
};
},
generatePullRequestDescription: async (
directory: string,
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
): Promise<GeneratedPullRequestDescription> => {
return sendBridgeMessage<GeneratedPullRequestDescription>('api:git/pr-description', {
directory,
base: payload.base,
head: payload.head,
context: payload.context,
zenModel: payload.zenModel,
providerId: payload.providerId,
modelId: payload.modelId,
});
},
listGitWorktrees: async (directory: string): Promise<GitWorktreeInfo[]> => {
return sendBridgeMessage<GitWorktreeInfo[]>('api:git/worktrees', { directory, method: 'GET' });
},
validateGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeValidationResult> => {
return sendBridgeMessage<GitWorktreeValidationResult>('api:git/worktrees/validate', {
directory,
...(payload || {}),
});
},
getGitWorktreeBootstrapStatus: async (directory: string): Promise<{ status: 'pending' | 'ready' | 'failed'; error: string | null; updatedAt: number }> => {
return sendBridgeMessage<{ status: 'pending' | 'ready' | 'failed'; error: string | null; updatedAt: number }>('api:git/worktrees/bootstrap-status', {
directory,
});
},
previewGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeCreateResult> => {
return sendBridgeMessage<GitWorktreeCreateResult>('api:git/worktrees/preview', {
directory,
method: 'POST',
...(payload || {}),
});
},
createGitWorktree: async (directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeCreateResult> => {
return sendBridgeMessage<GitWorktreeCreateResult>('api:git/worktrees', {
directory,
method: 'POST',
...(payload || {}),
});
},
deleteGitWorktree: async (directory: string, payload: RemoveGitWorktreePayload): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/worktrees', {
directory,
method: 'DELETE',
body: {
directory: payload.directory,
deleteLocalBranch: payload.deleteLocalBranch === true,
},
});
},
createGitCommit: async (directory: string, message: string, options?: CreateGitCommitOptions): Promise<GitCommitResult> => {
return sendBridgeMessage<GitCommitResult>('api:git/commit', {
directory,
message,
addAll: options?.addAll,
files: options?.files,
});
},
gitPush: async (directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record<string, unknown> }): Promise<GitPushResult> => {
return sendBridgeMessage<GitPushResult>('api:git/push', {
directory,
remote: options?.remote,
branch: options?.branch,
options: options?.options,
});
},
gitPull: async (directory: string, options?: { remote?: string; branch?: string; rebase?: boolean }): Promise<GitPullResult> => {
return sendBridgeMessage<GitPullResult>('api:git/pull', {
directory,
remote: options?.remote,
branch: options?.branch,
rebase: options?.rebase,
});
},
gitFetch: async (directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/fetch', {
directory,
remote: options?.remote,
branch: options?.branch,
});
},
checkoutBranch: async (directory: string, branch: string): Promise<{ success: boolean; branch: string }> => {
return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/checkout', {
directory,
branch,
});
},
createBranch: async (directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }> => {
return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/branches', {
directory,
method: 'POST',
name,
startPoint,
});
},
renameBranch: async (directory: string, oldName: string, newName: string): Promise<{ success: boolean; branch: string }> => {
return sendBridgeMessage<{ success: boolean; branch: string }>('api:git/branches/rename', {
directory,
method: 'PUT',
oldName,
newName,
});
},
getGitLog: async (directory: string, options?: GitLogOptions): Promise<GitLogResponse> => {
return sendBridgeMessage<GitLogResponse>('api:git/log', {
directory,
maxCount: options?.maxCount,
from: options?.from,
to: options?.to,
file: options?.file,
});
},
getCommitFiles: async (directory: string, hash: string): Promise<GitCommitFilesResponse> => {
return sendBridgeMessage<GitCommitFilesResponse>('api:git/commit-files', {
directory,
hash,
});
},
getCurrentGitIdentity: async (directory: string): Promise<GitIdentitySummary | null> => {
return sendBridgeMessage<GitIdentitySummary | null>('api:git/identity', {
directory,
method: 'GET',
});
},
setGitIdentity: async (directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> => {
// For VS Code, we need to resolve the profile from the store
// This is a simplified implementation - the full implementation would need profile lookup
return {
success: false,
profile: { id: profileId, name: '', userName: '', userEmail: '' },
};
},
// Git identity profile management - these are stored in extension settings
// For simplicity, return empty arrays/objects as these are managed through the settings UI
getGitIdentities: async (): Promise<GitIdentityProfile[]> => {
return [];
},
createGitIdentity: async (profile: GitIdentityProfile): Promise<GitIdentityProfile> => {
return profile;
},
updateGitIdentity: async (id: string, profile: GitIdentityProfile): Promise<GitIdentityProfile> => {
void id; // Unused for now
return profile;
},
deleteGitIdentity: async (id: string): Promise<void> => {
void id; // Unused for now
},
getRemotes: async (directory: string): Promise<GitRemote[]> => {
return sendBridgeMessage<GitRemote[]>('api:git/remotes', { directory });
},
rebase: async (directory: string, options: { onto: string }): Promise<GitRebaseResult> => {
return sendBridgeMessage<GitRebaseResult>('api:git/rebase', {
directory,
onto: options.onto,
});
},
abortRebase: async (directory: string): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/rebase/abort', { directory });
},
merge: async (directory: string, options: { branch: string }): Promise<GitMergeResult> => {
return sendBridgeMessage<GitMergeResult>('api:git/merge', {
directory,
branch: options.branch,
});
},
abortMerge: async (directory: string): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/merge/abort', { directory });
},
continueRebase: async (directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> => {
return sendBridgeMessage<{ success: boolean; conflict: boolean; conflictFiles?: string[] }>('api:git/rebase/continue', { directory });
},
continueMerge: async (directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> => {
return sendBridgeMessage<{ success: boolean; conflict: boolean; conflictFiles?: string[] }>('api:git/merge/continue', { directory });
},
stash: async (
directory: string,
options?: { message?: string; includeUntracked?: boolean }
): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/stash', {
directory,
...options,
});
},
stashPop: async (directory: string): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/stash/pop', { directory });
},
getConflictDetails: async (directory: string) => {
return sendBridgeMessage<{
statusPorcelain: string;
unmergedFiles: string[];
diff: string;
headInfo: string;
operation: 'merge' | 'rebase';
}>('api:git/conflict-details', { directory });
},
validateWorktreeDirectory: async (directory: string, worktreeRoot: string): Promise<{
valid: boolean;
insideWorktreeRoot: boolean;
resolvedWorktreeRoot: string | null;
resolvedCwd: string | null;
}> => {
return sendBridgeMessage<{
valid: boolean;
insideWorktreeRoot: boolean;
resolvedWorktreeRoot: string | null;
resolvedCwd: string | null;
}>('api:git/validate-directory', { directory, worktreeRoot });
},
canonicalizeWorktreeState: async (directory: string): Promise<{
worktreeRoot: string | null;
cwd: string | null;
branch: string | null;
headState: 'branch' | 'detached' | 'unborn';
worktreeStatus: 'ready' | 'missing' | 'invalid' | 'not-a-repo';
legacy: boolean;
degraded: boolean;
attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null;
}> => {
return sendBridgeMessage<{
worktreeRoot: string | null;
cwd: string | null;
branch: string | null;
headState: 'branch' | 'detached' | 'unborn';
worktreeStatus: 'ready' | 'missing' | 'invalid' | 'not-a-repo';
legacy: boolean;
degraded: boolean;
attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null;
}>('api:git/canonicalize-worktree-state', { directory });
},
worktree: {
list: async (directory: string): Promise<GitWorktreeInfo[]> => {
return sendBridgeMessage<GitWorktreeInfo[]>('api:git/worktrees', { directory, method: 'GET' });
},
validate: async (directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeValidationResult> => {
return sendBridgeMessage<GitWorktreeValidationResult>('api:git/worktrees/validate', {
directory,
...(payload || {}),
});
},
create: async (directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeCreateResult> => {
return sendBridgeMessage<GitWorktreeCreateResult>('api:git/worktrees', {
directory,
method: 'POST',
...(payload || {}),
});
},
remove: async (directory: string, payload: RemoveGitWorktreePayload): Promise<{ success: boolean }> => {
return sendBridgeMessage<{ success: boolean }>('api:git/worktrees', {
directory,
method: 'DELETE',
body: {
directory: payload.directory,
deleteLocalBranch: payload.deleteLocalBranch === true,
},
});
},
},
});