From 7b4bbb99e06bc451ce203d45db2ff3bd6448bb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB?= Date: Thu, 27 Aug 2026 15:55:03 +0200 Subject: [PATCH] refactor(services): extract Git operations into dedicated service with multi-repo support --- src/extension.ts | 102 +++++++++++++++++++++++------------------ src/gitService.ts | 76 +++++++++++++++++------------- src/opencodeService.ts | 15 +++--- 3 files changed, 110 insertions(+), 83 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 38d0147..7f873d9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,50 +1,32 @@ import * as vscode from "vscode"; import { generateCommitMessage, isOpenCodeAvailable } from "./opencodeService"; - -interface GitExtension { - getAPI(version: number): GitAPI; -} - -interface GitAPI { - repositories: Repository[]; -} - -interface Repository { - root: string; - rootUri: vscode.Uri; - state: RepositoryState; - inputBox: { value: string }; - diffIndexWithHEAD(path?: string): Promise; - diffWithHEAD(path?: string): Promise; - status(): Promise; -} - -interface RepositoryState { - indexChanges: any[]; - workingTreeChanges: any[]; -} +import { + Repository, + getAllRepositories, + getRepositoryLabel, + getRepositoryRoot, +} from "./gitService"; export async function activate( context: vscode.ExtensionContext, ): Promise { const commandHandler = vscode.commands.registerCommand( "aiCommitExt.generate", - async () => { - await handleGenerateCommitMessage(); + async (sc?: vscode.SourceControl) => { + await handleGenerateCommitMessage(sc); }, ); context.subscriptions.push(commandHandler); } -async function handleGenerateCommitMessage(): Promise { +async function handleGenerateCommitMessage( + sc?: vscode.SourceControl, +): Promise { const config = vscode.workspace.getConfiguration("aiCommitExt"); const showNotification = config.get("showNotification", true); try { - const [opencodeAvailable, gitExtension] = await Promise.all([ - isOpenCodeAvailable(), - vscode.extensions.getExtension("vscode.git"), - ]); + const opencodeAvailable = await isOpenCodeAvailable(); if (!opencodeAvailable) { if (showNotification) { @@ -55,25 +37,15 @@ async function handleGenerateCommitMessage(): Promise { return; } - if (!gitExtension) { - if (showNotification) { - vscode.window.showErrorMessage("Git extension not found"); - } - return; - } + const repository = await resolveRepository(sc); - const gitApi = gitExtension.exports.getAPI(1); - const repositories = gitApi.repositories; - - if (repositories.length === 0) { + if (!repository) { if (showNotification) { vscode.window.showErrorMessage("No Git repository found"); } return; } - const repository = repositories[0]; - await repository.status(); const state = repository.state; @@ -98,21 +70,31 @@ async function handleGenerateCommitMessage(): Promise { return; } + const repoLabel = getRepositoryLabel(repository); + const commitMessage = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, cancellable: true, - title: userSuggestion ? "Improving commit message..." : "Generating commit message...", + title: userSuggestion + ? `Improving commit message (${repoLabel})...` + : `Generating commit message (${repoLabel})...`, }, async (_progress, token) => { - return await generateCommitMessage({ userSuggestion: userSuggestion || undefined, cancellationToken: token }); + return await generateCommitMessage({ + repository, + userSuggestion: userSuggestion || undefined, + cancellationToken: token, + }); }, ); repository.inputBox.value = commitMessage; if (showNotification) { - vscode.window.showInformationMessage("Commit message generated"); + vscode.window.showInformationMessage( + `Commit message generated for ${repoLabel}`, + ); } } catch (error) { const message = @@ -125,6 +107,36 @@ async function handleGenerateCommitMessage(): Promise { } } +async function resolveRepository( + sc?: vscode.SourceControl, +): Promise { + const repositories = getAllRepositories(); + + if (repositories.length === 0) { + return undefined; + } + + if (sc) { + return repositories.find((r) => r.inputBox === sc.inputBox); + } + + if (repositories.length === 1) { + return repositories[0]; + } + + const items = repositories.map((repo) => ({ + label: getRepositoryLabel(repo), + description: getRepositoryRoot(repo), + repository: repo, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: "Select a repository to generate a commit message for", + }); + + return selected?.repository; +} + export function deactivate(): void {} export const output = vscode.window.createOutputChannel("ai-commit-ext"); diff --git a/src/gitService.ts b/src/gitService.ts index 6324d6e..b2622b1 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -1,24 +1,24 @@ import * as vscode from "vscode"; -interface GitExtension { +export interface GitExtension { getAPI(version: number): GitAPI; } -interface GitAPI { +export interface GitAPI { repositories: Repository[]; } -interface Repository { +export interface Repository { root: string; rootUri: vscode.Uri; state: RepositoryState; inputBox: { value: string }; - diffWithHEAD(path?: string): Promise; diffIndexWithHEAD(path?: string): Promise; + diffWithHEAD(path?: string): Promise; status(): Promise; } -interface RepositoryState { +export interface RepositoryState { indexChanges: Change[]; workingTreeChanges: Change[]; } @@ -34,12 +34,9 @@ export interface GitChange { diff?: string; } -export async function getGitChanges(): Promise { - const repository = await getActiveGitRepository(); - if (!repository) { - throw new Error("No Git repository found"); - } - +export async function getGitChanges( + repository: Repository, +): Promise { const state = repository.state; const changes: GitChange[] = []; @@ -70,12 +67,9 @@ export async function getGitChanges(): Promise { return changes; } -export async function getGitDiff(): Promise { - const repository = await getActiveGitRepository(); - if (!repository) { - throw new Error("No Git repository found"); - } - +export async function getGitDiff( + repository: Repository, +): Promise { const state = repository.state; let diffOutput = ""; @@ -123,7 +117,20 @@ export async function getGitDiff(): Promise { return diffOutput; } -async function getActiveGitRepository(): Promise { +export function getRepositoryRoot(repository: Repository): string { + if (typeof repository.root === "string") { + return repository.root; + } + return repository.rootUri.fsPath; +} + +export function getRepositoryLabel(repository: Repository): string { + const root = getRepositoryRoot(repository); + const folderName = root.split("/").pop() || root; + return folderName; +} + +export function getAllRepositories(): Repository[] { const gitExtension = vscode.extensions.getExtension("vscode.git"); if (!gitExtension) { @@ -131,10 +138,26 @@ async function getActiveGitRepository(): Promise { } const api = gitExtension.exports.getAPI(1); - if (api.repositories.length === 0) { - return null; + return api.repositories; +} + +export function findRepositoryForFile( + repositories: Repository[], + filePath: string, +): Repository | undefined { + const normalizedPath = filePath.toLowerCase(); + let bestMatch: Repository | undefined; + let bestLength = 0; + + for (const repo of repositories) { + const root = getRepositoryRoot(repo).toLowerCase(); + if (normalizedPath.startsWith(root) && root.length > bestLength) { + bestMatch = repo; + bestLength = root.length; + } } - return api.repositories[0]; + + return bestMatch; } function getChangeStatus(status: number): GitChange["status"] { @@ -160,14 +183,3 @@ function getChangeStatus(status: number): GitChange["status"] { return "modified"; } - -export async function getRepositoryRoot(): Promise { - const repository = await getActiveGitRepository(); - if (!repository) { - return null; - } - if (typeof repository.root === "string") { - return repository.root; - } - return repository.rootUri.fsPath; -} diff --git a/src/opencodeService.ts b/src/opencodeService.ts index a28d6b8..7b1b861 100644 --- a/src/opencodeService.ts +++ b/src/opencodeService.ts @@ -1,9 +1,10 @@ import { exec, spawn, ExecException } from "child_process"; import * as vscode from "vscode"; -import { getGitDiff, getRepositoryRoot } from "./gitService"; +import { getGitDiff, getRepositoryRoot, Repository } from "./gitService"; import { output } from "./extension"; export interface GenerateOptions { + repository: Repository; model?: string; userSuggestion?: string; cancellationToken?: vscode.CancellationToken; @@ -52,14 +53,16 @@ async function getOpenCodePath(): Promise { } export async function generateCommitMessage( - options: GenerateOptions = {}, + options: GenerateOptions, ): Promise { - const [opencodePath, diff, repoRoot] = await Promise.all([ + const { repository } = options; + + const [opencodePath, diff] = await Promise.all([ getOpenCodePath(), - getGitDiff(), - getRepositoryRoot(), + getGitDiff(repository), ]); + const repoRoot = getRepositoryRoot(repository); const config = vscode.workspace.getConfiguration("aiCommitExt"); const model = options.model || config.get("model", ""); @@ -103,7 +106,7 @@ Generate a concise Conventional Commit message for these changes:`; const proc = spawn(opencodePath, args, { stdio: ["pipe", "pipe", "pipe"], shell: true, - cwd: repoRoot || undefined, + cwd: repoRoot, env: { ...process.env, OPENCODE_SERVER_PASSWORD: undefined,