Add a Stashes dialog with create, apply, pop, and drop actions Include untracked files automatically when stashing Show file counts for current changes and stash entries
804 lines
28 KiB
TypeScript
804 lines
28 KiB
TypeScript
|
|
|
|
import type { RuntimeAPIs } from './api/types';
|
|
import * as gitHttp from './gitApiHttp';
|
|
import { opencodeClient } from './opencode/client';
|
|
import { renderMagicPrompt } from './magicPrompts';
|
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
|
import { useContextStore } from '@/stores/contextStore';
|
|
import { useConfigStore } from '@/stores/useConfigStore';
|
|
|
|
export type {
|
|
GitStatus,
|
|
GitDiffResponse,
|
|
GetGitDiffOptions,
|
|
GitBranchDetails,
|
|
GitBranch,
|
|
GitCommitResult,
|
|
GitPushResult,
|
|
GitPullResult,
|
|
GitIdentityProfile,
|
|
GitIdentityAuthType,
|
|
GitIdentitySummary,
|
|
GitLogEntry,
|
|
GitLogResponse,
|
|
GitWorktreeInfo,
|
|
CreateGitWorktreePayload,
|
|
GitWorktreeCreateResult,
|
|
RemoveGitWorktreePayload,
|
|
GitWorktreeValidationError,
|
|
GitWorktreeValidationResult,
|
|
GitDeleteBranchPayload,
|
|
GitDeleteRemoteBranchPayload,
|
|
GitRemoveRemotePayload,
|
|
DiscoveredGitCredential,
|
|
GitRemote,
|
|
GitMergeResult,
|
|
GitRebaseResult,
|
|
MergeConflictDetails,
|
|
} from './api/types';
|
|
|
|
declare global {
|
|
interface Window {
|
|
__OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
|
|
}
|
|
}
|
|
|
|
const getRuntimeGit = () => {
|
|
if (typeof window !== 'undefined' && window.__OPENCHAMBER_RUNTIME_APIS__?.git) {
|
|
return window.__OPENCHAMBER_RUNTIME_APIS__.git;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const requestChatForceScrollBottom = (sessionId: string) => {
|
|
if (typeof window === 'undefined') return;
|
|
window.dispatchEvent(new CustomEvent('openchamber:chat-force-scroll-bottom', {
|
|
detail: { sessionId },
|
|
}));
|
|
};
|
|
|
|
export async function checkIsGitRepository(directory: string): Promise<boolean> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.checkIsGitRepository(directory);
|
|
return gitHttp.checkIsGitRepository(directory);
|
|
}
|
|
|
|
export async function getGitStatus(directory: string, options?: { mode?: 'light' }): Promise<import('./api/types').GitStatus> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitStatus(directory);
|
|
return gitHttp.getGitStatus(directory, options);
|
|
}
|
|
|
|
export async function getGitDiff(directory: string, options: import('./api/types').GetGitDiffOptions): Promise<import('./api/types').GitDiffResponse> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitDiff(directory, options);
|
|
return gitHttp.getGitDiff(directory, options);
|
|
}
|
|
|
|
export async function getGitFileDiff(
|
|
directory: string,
|
|
options: import('./api/types').GetGitFileDiffOptions
|
|
): Promise<import('./api/types').GitFileDiffResponse> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitFileDiff(directory, options);
|
|
return gitHttp.getGitFileDiff(directory, options);
|
|
}
|
|
|
|
export async function revertGitFile(directory: string, filePath: string): Promise<void> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.revertGitFile(directory, filePath);
|
|
return gitHttp.revertGitFile(directory, filePath);
|
|
}
|
|
|
|
export async function isLinkedWorktree(directory: string): Promise<boolean> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.isLinkedWorktree(directory);
|
|
return gitHttp.isLinkedWorktree(directory);
|
|
}
|
|
|
|
export async function getGitBranches(directory: string): Promise<import('./api/types').GitBranch> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitBranches(directory);
|
|
return gitHttp.getGitBranches(directory);
|
|
}
|
|
|
|
export async function deleteGitBranch(directory: string, payload: import('./api/types').GitDeleteBranchPayload): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.deleteGitBranch(directory, payload);
|
|
return gitHttp.deleteGitBranch(directory, payload);
|
|
}
|
|
|
|
export async function deleteRemoteBranch(directory: string, payload: import('./api/types').GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.deleteRemoteBranch(directory, payload);
|
|
return gitHttp.deleteRemoteBranch(directory, payload);
|
|
}
|
|
|
|
export async function generateCommitMessage(
|
|
directory: string,
|
|
files: string[],
|
|
options?: { zenModel?: string; providerId?: string; modelId?: string }
|
|
): Promise<{ message: import('./api/types').GeneratedCommitMessage }> {
|
|
const startedAt = Date.now();
|
|
void options;
|
|
const generationSession = resolveSessionGenerationContext();
|
|
|
|
if (!generationSession) {
|
|
throw new Error('Select existing session for generation');
|
|
}
|
|
|
|
console.info('[git-generation][browser] request', {
|
|
transport: 'session',
|
|
kind: 'commit',
|
|
directory,
|
|
selectedFiles: files.length,
|
|
sessionId: generationSession.sessionId,
|
|
providerId: generationSession.providerID,
|
|
modelId: generationSession.modelID,
|
|
agent: generationSession.agent,
|
|
});
|
|
|
|
const visiblePrompt = await renderMagicPrompt('git.commit.generate.visible');
|
|
const hiddenPrompt = await renderMagicPrompt('git.commit.generate.instructions', {
|
|
selected_files: files.map((file) => `- ${file}`).join('\n'),
|
|
});
|
|
|
|
try {
|
|
const structured = await runStructuredGenerationInActiveSession({
|
|
directory,
|
|
visiblePrompt,
|
|
hiddenPrompt,
|
|
generationSession,
|
|
schema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
subject: { type: 'string', description: 'Conventional commit subject line.' },
|
|
highlights: {
|
|
type: 'array',
|
|
items: { type: 'string', description: 'Short user-facing highlight.' },
|
|
maxItems: 3,
|
|
description: 'Optional short user-facing highlights.',
|
|
},
|
|
},
|
|
required: ['subject', 'highlights'],
|
|
},
|
|
kind: 'commit',
|
|
});
|
|
|
|
const subject = typeof structured.subject === 'string' ? structured.subject.trim() : '';
|
|
const highlights = Array.isArray(structured.highlights)
|
|
? structured.highlights.filter((item) => typeof item === 'string').map((item) => item.trim()).filter(Boolean).slice(0, 3)
|
|
: [];
|
|
|
|
if (!subject) {
|
|
throw new Error('Structured output missing subject');
|
|
}
|
|
|
|
const result = { message: { subject, highlights } };
|
|
console.info('[git-generation][browser] success', {
|
|
transport: 'session',
|
|
kind: 'commit',
|
|
elapsedMs: Date.now() - startedAt,
|
|
subjectLength: result.message.subject.length,
|
|
highlightsCount: result.message.highlights.length,
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[git-generation][browser] failed', {
|
|
transport: 'session',
|
|
kind: 'commit',
|
|
elapsedMs: Date.now() - startedAt,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
error,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function generatePullRequestDescription(
|
|
directory: string,
|
|
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
|
|
): Promise<import('./api/types').GeneratedPullRequestDescription> {
|
|
const startedAt = Date.now();
|
|
const generationSession = resolveSessionGenerationContext();
|
|
if (!generationSession) {
|
|
throw new Error('Select existing session for generation');
|
|
}
|
|
|
|
const commitLog = await getGitLog(directory, {
|
|
from: payload.base,
|
|
to: payload.head,
|
|
maxCount: 50,
|
|
});
|
|
const commits = (Array.isArray(commitLog?.all) ? commitLog.all : [])
|
|
.filter((entry) => typeof entry?.hash === 'string' && entry.hash.length > 0)
|
|
.map((entry) => ({
|
|
hash: entry.hash,
|
|
subject: typeof entry.message === 'string' ? entry.message.trim() : '',
|
|
}));
|
|
|
|
if (commits.length === 0) {
|
|
throw new Error(`No commits found in range ${payload.base}...${payload.head}`);
|
|
}
|
|
|
|
const filesSet = new Set<string>();
|
|
await Promise.all(commits.map(async (commit) => {
|
|
try {
|
|
const response = await getCommitFiles(directory, commit.hash);
|
|
const files = Array.isArray(response?.files) ? response.files : [];
|
|
for (const file of files) {
|
|
if (typeof file?.path === 'string' && file.path.trim().length > 0) {
|
|
filesSet.add(file.path.trim());
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('[git-generation][browser] failed to collect commit files', {
|
|
hash: commit.hash,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}));
|
|
const changedFiles = Array.from(filesSet).sort().slice(0, 300);
|
|
|
|
console.info('[git-generation][browser] request', {
|
|
transport: 'session',
|
|
kind: 'pr',
|
|
directory,
|
|
sessionId: generationSession.sessionId,
|
|
providerId: generationSession.providerID,
|
|
modelId: generationSession.modelID,
|
|
agent: generationSession.agent,
|
|
base: payload.base,
|
|
head: payload.head,
|
|
commits: commits.length,
|
|
changedFiles: changedFiles.length,
|
|
});
|
|
|
|
const visiblePrompt = await renderMagicPrompt('git.pr.generate.visible');
|
|
const hiddenPrompt = await renderMagicPrompt('git.pr.generate.instructions', {
|
|
base_branch: payload.base,
|
|
head_branch: payload.head,
|
|
commits: commits.map((commit) => `- ${commit.hash.slice(0, 7)} ${commit.subject || '(no subject)'}`).join('\n'),
|
|
changed_files: changedFiles.length > 0 ? changedFiles.map((file) => `- ${file}`).join('\n') : '- none detected',
|
|
additional_context_block: payload.context?.trim() ? `\nAdditional context:\n${payload.context.trim()}` : '',
|
|
});
|
|
|
|
try {
|
|
const structured = await runStructuredGenerationInActiveSession({
|
|
directory,
|
|
visiblePrompt,
|
|
hiddenPrompt,
|
|
generationSession,
|
|
schema: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
title: { type: 'string', description: 'Pull request title.' },
|
|
body: { type: 'string', description: 'Pull request markdown description.' },
|
|
},
|
|
required: ['title', 'body'],
|
|
},
|
|
kind: 'pr',
|
|
});
|
|
|
|
const result = {
|
|
title: typeof structured.title === 'string' ? structured.title.trim() : '',
|
|
body: typeof structured.body === 'string' ? structured.body.trim() : '',
|
|
};
|
|
console.info('[git-generation][browser] success', {
|
|
transport: 'session',
|
|
kind: 'pr',
|
|
elapsedMs: Date.now() - startedAt,
|
|
titleLength: result.title.length,
|
|
bodyLength: result.body.length,
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[git-generation][browser] failed', {
|
|
transport: 'session',
|
|
kind: 'pr',
|
|
elapsedMs: Date.now() - startedAt,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
error,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
type SessionGenerationContext = {
|
|
sessionId: string;
|
|
providerID: string;
|
|
modelID: string;
|
|
agent?: string;
|
|
};
|
|
|
|
const resolveSessionGenerationContext = (): SessionGenerationContext | null => {
|
|
const sessionId = useSessionUIStore.getState().currentSessionId;
|
|
if (!sessionId) {
|
|
return null;
|
|
}
|
|
|
|
const context = useContextStore.getState();
|
|
const config = useConfigStore.getState();
|
|
|
|
const agent = context.getSessionAgentSelection(sessionId) || config.currentAgentName || undefined;
|
|
const sessionModel = context.getSessionModelSelection(sessionId);
|
|
const agentModel = agent ? context.getAgentModelForSession(sessionId, agent) : null;
|
|
const selectedModel = agentModel || sessionModel || (config.currentProviderId && config.currentModelId
|
|
? { providerId: config.currentProviderId, modelId: config.currentModelId }
|
|
: null);
|
|
|
|
if (!selectedModel?.providerId || !selectedModel?.modelId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
sessionId,
|
|
providerID: selectedModel.providerId,
|
|
modelID: selectedModel.modelId,
|
|
agent,
|
|
};
|
|
};
|
|
|
|
const runStructuredGenerationInActiveSession = async ({
|
|
directory,
|
|
visiblePrompt,
|
|
hiddenPrompt,
|
|
generationSession,
|
|
schema,
|
|
kind,
|
|
}: {
|
|
directory: string;
|
|
visiblePrompt: string;
|
|
hiddenPrompt?: string;
|
|
generationSession: SessionGenerationContext;
|
|
schema: Record<string, unknown>;
|
|
kind: 'commit' | 'pr';
|
|
}): Promise<Record<string, unknown>> => {
|
|
const requestStartedAt = Date.now();
|
|
console.info('[git-generation][browser] runStructuredGenerationInActiveSession start', {
|
|
kind,
|
|
directory,
|
|
sessionId: generationSession.sessionId,
|
|
providerID: generationSession.providerID,
|
|
modelID: generationSession.modelID,
|
|
agent: generationSession.agent,
|
|
});
|
|
const trimmedDirectory = typeof directory === 'string' ? directory.trim() : '';
|
|
const visiblePromptText = typeof visiblePrompt === 'string' ? visiblePrompt.trim() : '';
|
|
const hiddenPromptText = typeof hiddenPrompt === 'string' ? hiddenPrompt.trim() : '';
|
|
const promptParts: Array<{ type: 'text'; text: string; synthetic?: boolean }> = [];
|
|
if (visiblePromptText) {
|
|
promptParts.push({ type: 'text', text: visiblePromptText, synthetic: false });
|
|
}
|
|
if (hiddenPromptText) {
|
|
promptParts.push({ type: 'text', text: hiddenPromptText, synthetic: true });
|
|
}
|
|
if (promptParts.length === 0) {
|
|
throw new Error('Generation prompts are empty');
|
|
}
|
|
|
|
requestChatForceScrollBottom(generationSession.sessionId);
|
|
|
|
const response = await opencodeClient.withDirectory(directory, async () => {
|
|
return opencodeClient.getApiClient().session.prompt({
|
|
sessionID: generationSession.sessionId,
|
|
...(trimmedDirectory.length > 0 ? { directory: trimmedDirectory } : {}),
|
|
model: {
|
|
providerID: generationSession.providerID,
|
|
modelID: generationSession.modelID,
|
|
},
|
|
...(generationSession.agent ? { agent: generationSession.agent } : {}),
|
|
format: {
|
|
type: 'json_schema',
|
|
schema,
|
|
retryCount: 2,
|
|
},
|
|
parts: promptParts,
|
|
});
|
|
});
|
|
|
|
const responseError = response?.error as { message?: string } | undefined;
|
|
if (!response?.data) {
|
|
throw new Error(responseError?.message || `Failed to generate ${kind} output`);
|
|
}
|
|
|
|
const info = response.data.info as { finish?: string; structured_output?: unknown; structured?: unknown; error?: unknown };
|
|
const structuredOutput = info?.structured_output || info?.structured;
|
|
if (!structuredOutput || typeof structuredOutput !== 'object' || Array.isArray(structuredOutput)) {
|
|
console.error('[git-generation][browser] invalid structured output', {
|
|
kind,
|
|
sessionId: generationSession.sessionId,
|
|
elapsedMs: Date.now() - requestStartedAt,
|
|
finish: info?.finish,
|
|
messageInfo: response.data.info,
|
|
messageParts: response.data.parts,
|
|
});
|
|
throw new Error('No structured output returned by session');
|
|
}
|
|
|
|
return structuredOutput as Record<string, unknown>;
|
|
};
|
|
|
|
export async function listGitWorktrees(directory: string): Promise<import('./api/types').GitWorktreeInfo[]> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.list) {
|
|
return runtime.worktree.list(directory);
|
|
}
|
|
if (runtime) return runtime.listGitWorktrees(directory);
|
|
return gitHttp.listGitWorktrees(directory);
|
|
}
|
|
|
|
export async function validateGitWorktree(
|
|
directory: string,
|
|
payload: import('./api/types').CreateGitWorktreePayload
|
|
): Promise<import('./api/types').GitWorktreeValidationResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.validate) {
|
|
return runtime.worktree.validate(directory, payload);
|
|
}
|
|
if (runtime?.validateGitWorktree) {
|
|
return runtime.validateGitWorktree(directory, payload);
|
|
}
|
|
return gitHttp.validateGitWorktree(directory, payload);
|
|
}
|
|
|
|
export async function getGitWorktreeBootstrapStatus(
|
|
directory: string,
|
|
): Promise<import('./api/types').GitWorktreeBootstrapStatus> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.bootstrapStatus) {
|
|
return runtime.worktree.bootstrapStatus(directory);
|
|
}
|
|
if (runtime?.getGitWorktreeBootstrapStatus) {
|
|
return runtime.getGitWorktreeBootstrapStatus(directory);
|
|
}
|
|
return gitHttp.getGitWorktreeBootstrapStatus(directory);
|
|
}
|
|
|
|
export async function previewGitWorktree(
|
|
directory: string,
|
|
payload: import('./api/types').CreateGitWorktreePayload
|
|
): Promise<import('./api/types').GitWorktreeCreateResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.preview) {
|
|
return runtime.worktree.preview(directory, payload);
|
|
}
|
|
if (runtime?.previewGitWorktree) {
|
|
return runtime.previewGitWorktree(directory, payload);
|
|
}
|
|
return gitHttp.previewGitWorktree(directory, payload);
|
|
}
|
|
|
|
export async function createGitWorktree(
|
|
directory: string,
|
|
payload: import('./api/types').CreateGitWorktreePayload
|
|
): Promise<import('./api/types').GitWorktreeCreateResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.create) {
|
|
return runtime.worktree.create(directory, payload);
|
|
}
|
|
if (runtime?.createGitWorktree) {
|
|
return runtime.createGitWorktree(directory, payload);
|
|
}
|
|
return gitHttp.createGitWorktree(directory, payload);
|
|
}
|
|
|
|
export async function deleteGitWorktree(
|
|
directory: string,
|
|
payload: import('./api/types').RemoveGitWorktreePayload
|
|
): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.worktree?.remove) {
|
|
return runtime.worktree.remove(directory, payload);
|
|
}
|
|
if (runtime?.deleteGitWorktree) {
|
|
return runtime.deleteGitWorktree(directory, payload);
|
|
}
|
|
return gitHttp.deleteGitWorktree(directory, payload);
|
|
}
|
|
|
|
export const git = {
|
|
worktree: {
|
|
list: listGitWorktrees,
|
|
validate: validateGitWorktree,
|
|
create: createGitWorktree,
|
|
remove: deleteGitWorktree,
|
|
},
|
|
};
|
|
|
|
export async function createGitCommit(
|
|
directory: string,
|
|
message: string,
|
|
options: import('./api/types').CreateGitCommitOptions = {}
|
|
): Promise<import('./api/types').GitCommitResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.createGitCommit(directory, message, options);
|
|
return gitHttp.createGitCommit(directory, message, options);
|
|
}
|
|
|
|
export async function gitPush(
|
|
directory: string,
|
|
options: { remote?: string; branch?: string; options?: string[] | Record<string, unknown> } = {}
|
|
): Promise<import('./api/types').GitPushResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.gitPush(directory, options);
|
|
return gitHttp.gitPush(directory, options);
|
|
}
|
|
|
|
export async function gitPull(
|
|
directory: string,
|
|
options: import('./api/types').GitPullOptions = {}
|
|
): Promise<import('./api/types').GitPullResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.gitPull(directory, options);
|
|
return gitHttp.gitPull(directory, options);
|
|
}
|
|
|
|
export async function gitFetch(
|
|
directory: string,
|
|
options: { remote?: string; branch?: string } = {}
|
|
): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.gitFetch(directory, options);
|
|
return gitHttp.gitFetch(directory, options);
|
|
}
|
|
|
|
export async function listGitStashes(directory: string): Promise<{ stashes: import('./api/types').GitStashEntry[] }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.listGitStashes(directory);
|
|
return gitHttp.listGitStashes(directory);
|
|
}
|
|
|
|
export async function countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record<string, number> }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.countGitStashFiles(directory, refs);
|
|
return gitHttp.countGitStashFiles(directory, refs);
|
|
}
|
|
|
|
export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.stashGitChanges(directory, options);
|
|
return gitHttp.stashGitChanges(directory, options);
|
|
}
|
|
|
|
export async function applyGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.applyGitStash(directory, options);
|
|
return gitHttp.applyGitStash(directory, options);
|
|
}
|
|
|
|
export async function popGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.popGitStash(directory, options);
|
|
return gitHttp.popGitStash(directory, options);
|
|
}
|
|
|
|
export async function dropGitStash(directory: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.dropGitStash(directory, options);
|
|
return gitHttp.dropGitStash(directory, options);
|
|
}
|
|
|
|
export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.checkoutBranch(directory, branch);
|
|
return gitHttp.checkoutBranch(directory, branch);
|
|
}
|
|
|
|
export async function createBranch(
|
|
directory: string,
|
|
name: string,
|
|
startPoint?: string
|
|
): Promise<{ success: boolean; branch: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.createBranch(directory, name, startPoint);
|
|
return gitHttp.createBranch(directory, name, startPoint);
|
|
}
|
|
|
|
export async function renameBranch(
|
|
directory: string,
|
|
oldName: string,
|
|
newName: string
|
|
): Promise<{ success: boolean; branch: string }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.renameBranch(directory, oldName, newName);
|
|
return gitHttp.renameBranch(directory, oldName, newName);
|
|
}
|
|
|
|
export async function getGitLog(
|
|
directory: string,
|
|
options: import('./api/types').GitLogOptions = {}
|
|
): Promise<import('./api/types').GitLogResponse> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitLog(directory, options);
|
|
return gitHttp.getGitLog(directory, options);
|
|
}
|
|
|
|
export async function getCommitFiles(
|
|
directory: string,
|
|
hash: string
|
|
): Promise<import('./api/types').GitCommitFilesResponse> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getCommitFiles(directory, hash);
|
|
return gitHttp.getCommitFiles(directory, hash);
|
|
}
|
|
|
|
export async function getGitIdentities(): Promise<import('./api/types').GitIdentityProfile[]> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getGitIdentities();
|
|
return gitHttp.getGitIdentities();
|
|
}
|
|
|
|
export async function createGitIdentity(profile: import('./api/types').GitIdentityProfile): Promise<import('./api/types').GitIdentityProfile> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.createGitIdentity(profile);
|
|
return gitHttp.createGitIdentity(profile);
|
|
}
|
|
|
|
export async function updateGitIdentity(id: string, updates: import('./api/types').GitIdentityProfile): Promise<import('./api/types').GitIdentityProfile> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.updateGitIdentity(id, updates);
|
|
return gitHttp.updateGitIdentity(id, updates);
|
|
}
|
|
|
|
export async function deleteGitIdentity(id: string): Promise<void> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.deleteGitIdentity(id);
|
|
return gitHttp.deleteGitIdentity(id);
|
|
}
|
|
|
|
export async function getCurrentGitIdentity(directory: string): Promise<import('./api/types').GitIdentitySummary | null> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getCurrentGitIdentity(directory);
|
|
return gitHttp.getCurrentGitIdentity(directory);
|
|
}
|
|
|
|
export async function hasLocalIdentity(directory: string): Promise<boolean> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.hasLocalIdentity) return runtime.hasLocalIdentity(directory);
|
|
return gitHttp.hasLocalIdentity(directory);
|
|
}
|
|
|
|
export async function setGitIdentity(
|
|
directory: string,
|
|
profileId: string
|
|
): Promise<{ success: boolean; profile: import('./api/types').GitIdentityProfile }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.setGitIdentity(directory, profileId);
|
|
return gitHttp.setGitIdentity(directory, profileId);
|
|
}
|
|
|
|
export async function discoverGitCredentials(): Promise<import('./api/types').DiscoveredGitCredential[]> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.discoverGitCredentials) return runtime.discoverGitCredentials();
|
|
return gitHttp.discoverGitCredentials();
|
|
}
|
|
|
|
export async function getGlobalGitIdentity(): Promise<import('./api/types').GitIdentitySummary | null> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.getGlobalGitIdentity) return runtime.getGlobalGitIdentity();
|
|
return gitHttp.getGlobalGitIdentity();
|
|
}
|
|
|
|
export async function getRemoteUrl(directory: string, remote?: string): Promise<string | null> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.getRemoteUrl) return runtime.getRemoteUrl(directory, remote);
|
|
return gitHttp.getRemoteUrl(directory, remote);
|
|
}
|
|
|
|
export async function getRemotes(directory: string): Promise<import('./api/types').GitRemote[]> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.getRemotes(directory);
|
|
return gitHttp.getRemotes(directory);
|
|
}
|
|
|
|
export async function removeRemote(
|
|
directory: string,
|
|
payload: import('./api/types').GitRemoveRemotePayload
|
|
): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.removeRemote(directory, payload);
|
|
return gitHttp.removeRemote(directory, payload);
|
|
}
|
|
|
|
export async function rebase(
|
|
directory: string,
|
|
options: { onto: string }
|
|
): Promise<import('./api/types').GitRebaseResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.rebase(directory, options);
|
|
return gitHttp.rebase(directory, options);
|
|
}
|
|
|
|
export async function abortRebase(directory: string): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.abortRebase(directory);
|
|
return gitHttp.abortRebase(directory);
|
|
}
|
|
|
|
export async function merge(
|
|
directory: string,
|
|
options: { branch: string }
|
|
): Promise<import('./api/types').GitMergeResult> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.merge(directory, options);
|
|
return gitHttp.merge(directory, options);
|
|
}
|
|
|
|
export async function abortMerge(directory: string): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.abortMerge(directory);
|
|
return gitHttp.abortMerge(directory);
|
|
}
|
|
|
|
export async function continueRebase(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.continueRebase(directory);
|
|
return gitHttp.continueRebase(directory);
|
|
}
|
|
|
|
export async function continueMerge(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.continueMerge(directory);
|
|
return gitHttp.continueMerge(directory);
|
|
}
|
|
|
|
export async function stash(
|
|
directory: string,
|
|
options?: { message?: string; includeUntracked?: boolean }
|
|
): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.stash(directory, options);
|
|
return gitHttp.stash(directory, options);
|
|
}
|
|
|
|
export async function stashPop(directory: string): Promise<{ success: boolean }> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime) return runtime.stashPop(directory);
|
|
return gitHttp.stashPop(directory);
|
|
}
|
|
|
|
export async function getConflictDetails(directory: string): Promise<import('./api/types').MergeConflictDetails> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.getConflictDetails) return runtime.getConflictDetails(directory);
|
|
return gitHttp.getConflictDetails(directory);
|
|
}
|
|
|
|
export async function validateWorktreeDirectory(
|
|
directory: string,
|
|
worktreeRoot: string
|
|
): Promise<{
|
|
valid: boolean;
|
|
insideWorktreeRoot: boolean;
|
|
resolvedWorktreeRoot: string | null;
|
|
resolvedCwd: string | null;
|
|
}> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.validateWorktreeDirectory) {
|
|
return runtime.validateWorktreeDirectory(directory, worktreeRoot);
|
|
}
|
|
return gitHttp.validateWorktreeDirectory(directory, worktreeRoot);
|
|
}
|
|
|
|
export async function canonicalizeWorktreeState(
|
|
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;
|
|
}> {
|
|
const runtime = getRuntimeGit();
|
|
if (runtime?.canonicalizeWorktreeState) {
|
|
return runtime.canonicalizeWorktreeState(directory);
|
|
}
|
|
return gitHttp.canonicalizeWorktreeState(directory);
|
|
}
|